Jackson 101: The Complete Java JSON Tutorial – Setup, ObjectMapper, and Best Practices

Jackson is the de facto standard for JSON processing in Java. It ships as the default JSON library in Spring Boot, is used by thousands of open-source frameworks, and handles everything from trivial string serialisation to streaming gigabyte-sized files. If you write Java and you touch JSON — which is essentially everyone — you will use Jackson. This post gives you the essential foundation: why Jackson dominates, how to set it up, and the one architectural rule that matters most.

Why Jackson, Not Gson?

Both Jackson and Gson are mature, actively maintained libraries. The key differences are:

FeatureJacksonGson
Spring Boot integrationBuilt-in default (zero config)Requires manual registration
PerformanceFaster on most benchmarks, especially large payloadsCompetitive on small payloads
Streaming APIYes — JsonParser / JsonGeneratorNo
Java 8+ type supportVia JavaTimeModule, Jdk8ModuleLimited, requires custom adapters
Polymorphic typesFirst-class with @JsonTypeInfoRequires manual type adapters
Module ecosystemLarge (YAML, XML, CSV, CBOR, etc.)Minimal

For greenfield projects on Spring Boot or Jakarta EE, Jackson is the correct default choice. Gson remains a reasonable option for Android or lightweight standalone utilities.

Maven and Gradle Setup

Maven:

<!-- Core: data binding, annotations, streaming -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>3.1.2</version>
</dependency>

<!-- Java 8 date/time support (LocalDate, Instant, etc.) -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>3.1.2</version>
</dependency>

Gradle:

implementation 'com.fasterxml.jackson.core:jackson-databind:3.1.2'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:3.1.2'

If you are using Spring Boot, both are included transitively via spring-boot-starter-web. You do not need to declare them explicitly unless you need to override the version.

The ObjectMapper Lifecycle — The One Rule That Matters Most

ObjectMapper is expensive to create. The first time you call readValue() or writeValueAsString() on a new type, Jackson inspects that type via reflection, builds an internal serialisation model, and caches it. On subsequent calls, the cached model is reused, making those calls fast.

Creating a new ObjectMapper per request throws away that cache and pays the reflection cost every single time. It also causes excessive garbage collection pressure under load.

Wrong — do not do this:

// BAD: creates a new ObjectMapper on every request
@PostMapping("/orders")
public ResponseEntity<OrderDto> createOrder(@RequestBody String rawJson) throws Exception {
    ObjectMapper mapper = new ObjectMapper(); // New instance per request = performance problem
    OrderDto order = mapper.readValue(rawJson, OrderDto.class);
    return ResponseEntity.ok(order);
}

Correct — create once, share everywhere:

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper sharedObjectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }
}

ObjectMapper is thread-safe once configured. A single bean shared across your entire application is the correct pattern.

Your First Serialise/Deserialise Example

The following shows the complete round-trip: a ProductSummary object serialised to a JSON string, then deserialised back to a Java object. This pattern applies to any POJO with standard getters and setters.

public class ProductSummary {
    private Long   productId;
    private String productName;
    private double listPrice;
    // Constructors, getters, setters
}

ObjectMapper mapper = new ObjectMapper();

// Serialise: Java object -> JSON string
ProductSummary product = new ProductSummary(1L, "Mechanical Keyboard", 79.99);
String jsonOutput = mapper.writeValueAsString(product);
System.out.println(jsonOutput);
// Output: {"productId":1,"productName":"Mechanical Keyboard","listPrice":79.99}

// Deserialise: JSON string -> Java object
ProductSummary restored = mapper.readValue(jsonOutput, ProductSummary.class);
System.out.println(restored.getProductName()); // Output: Mechanical Keyboard

Jackson’s Three Processing Models

Jackson offers three distinct APIs for different use cases:

  1. Data Binding (ObjectMapper) — Converts JSON directly to/from POJOs. Use for 95% of all tasks.
  2. Tree Model (JsonNode) — Represents JSON as a navigable tree. Use for dynamic or schema-unknown JSON.
  3. Streaming API (JsonParser / JsonGenerator) — Token-level access with no in-memory document. Use for large file processing.

This series covers all three in detail.

Series Index

This post is the entry point for the complete Jackson 3 series. Each part builds on the previous — start here if you are new to Jackson, or jump directly to whichever topic you need.

AI Prompts for Jackson 101 Setup

Scaffold JsonMapper Spring Bean

I am starting a new Spring Boot 3 project with Jackson 3.1.2. Generate a @Configuration class with a JsonMapper @Bean built using JsonMapper.builder(). The bean should disable WRITE_DATES_AS_TIMESTAMPS, set FAIL_ON_UNKNOWN_PROPERTIES to false, exclude null fields from serialisation, and be marked @Primary so Spring Boot’s HTTP message converters use it. Add a brief inline comment on each builder call explaining why that setting is applied.

What it does: Generates a complete, production-ready @Configuration class with a single immutable JsonMapper bean. Each builder call is annotated inline so the intent is clear to reviewers, and the @Primary marker ensures Spring Boot’s message converters use this bean without extra wiring.

When to use it: At project creation time to establish a single shared mapper before any serialisation code is written. Prevents the common anti-pattern of multiple ObjectMapper instances with inconsistent settings scattered across service classes.

Upgrade ObjectMapper to Jackson 3

I have this existing Jackson 2 @Configuration class: [paste your class here]. Migrate it to Jackson 3.1.2 using JsonMapper.builder(). Convert every setter-style call — setSerializationInclusion, configure, setVisibility — to its builder equivalent. Remove explicit registerModule calls for JavaTimeModule and Jdk8Module since they are auto-registered in Jackson 3. If enableDefaultTyping() is present, flag it as removed in Jackson 3 and suggest the @JsonTypeInfo replacement. Show before and after side by side.

What it does: Produces a line-by-line migration of your existing configuration class, converting mutable setter calls to immutable builder equivalents, removing redundant module registrations, and surfacing any removed APIs as clearly labelled blockers with their Jackson 3 safe replacements.

When to use it: When upgrading a Spring Boot application to Jackson 3.1.x and wanting a reviewer-friendly diff rather than a from-scratch rewrite. Especially useful when the configuration has grown through multiple contributors and contains a mix of feature flags, inclusion settings, and module registrations.

Jackson vs Gson Decision Guide

We are building a Spring Boot 3 microservice with Java 21 records as DTOs. Compare Jackson 3.1.2 and Gson 2.x across: Spring Boot auto-configuration support, Java 21 records and sealed classes support, module ecosystem breadth (YAML, CBOR, XML), serialisation performance on record types, security CVE history, and ongoing maintenance activity. Format the result as a comparison table followed by a one-paragraph recommendation suitable for an ADR.

What it does: Produces a structured comparison table with a concrete factual statement per cell, covering all six requested dimensions, followed by a recommendation paragraph that can be dropped directly into an Architecture Decision Record without editing.

When to use it: When justifying the Jackson choice to engineers from Android or Kotlin backgrounds where Gson is familiar, or when documenting the library selection rationale at the start of a new project.

Conclusion

Jackson is the clear choice for JSON in Java projects, particularly those running on Spring Boot. It delivers outstanding performance, a rich annotation model, and a broad module ecosystem. The one architectural principle to take away from this introduction is the ObjectMapper lifecycle: create it once, configure it at startup, and share it as a singleton. Everything else in this series builds on that foundation.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.