Jackson 3 vs Jackson 2: Complete Comparison of API Changes, Performance, and New Features

The first time I ran a Jackson 3.1.2 build against a codebase that had been on Jackson 2 since 2019, the compile output had three categories of error: removed methods I expected, removed methods I had completely forgotten about, and one that genuinely surprised me — a custom deserialiser that silently stopped working because it depended on mutable ObjectMapper reconfiguration after injection. Jackson 3 is not an incremental release. It raises the Java baseline to 17, renames the entire package namespace, makes mapper configuration immutable, inlines three modules that previously required separate dependencies, and removes the APIs most responsible for serialisation CVEs. The changes below are the ones that actually matter when you’re deciding whether to upgrade — not a spec recap, but the differences that will touch your code.

The Change That Will Actually Break Your Code First

Before the full comparison table: in every Jackson 3 upgrade I have seen, the first breaking change that isn’t a compile error is the mapper immutability model. If anywhere in your codebase you have code that retrieves an ObjectMapper bean and calls a configure method on it after instantiation — even once, in a test setup — that code is silently a no-op in Jackson 3. The mapper built by JsonMapper.builder().build() is locked. The call succeeds, nothing throws, but your configuration change has no effect. Tests pass. Production behaves differently. Audit every location where ObjectMapper is touched after construction before bumping the version.

At a Glance: Jackson 2 vs Jackson 3

FeatureJackson 2.x (2.17.2)Jackson 3.x (3.1.2)
Java baselineJava 8+Java 17+ (all core modules)
Package namespacecom.fasterxml.jackson.*tools.jackson.* (annotations stay at com.fasterxml.jackson.annotation)
Maven Group IDcom.fasterxml.jackson.coretools.jackson.core (annotations: com.fasterxml.jackson.core)
Primary mapper classObjectMapperJsonMapper (builder); ObjectMapper still present
Mapper constructionMutable: new ObjectMapper() + chained settersImmutable: JsonMapper.builder().build()
Exception hierarchyJsonProcessingException extends IOException (checked)JacksonException extends RuntimeException (unchecked)
Java recordsSupported from 2.12 via jackson-module-parameter-namesNative, zero extra dependency
Optional<T>Requires jackson-datatype-jdk8Built into core
java.time.* typesRequires explicit JavaTimeModule registrationAuto-registered
enableDefaultTyping()Deprecated in 2.10, removed in 2.16Does not exist — compile error
Security postureGadget attacks possible with misconfigurationTighter by default; removed dangerous APIs

Package and Group ID Rename: The Biggest Migration Task

This is the change that touches the most files in a real codebase. Jackson 3 moved its entire package namespace from com.fasterxml.jackson.* to tools.jackson.*, and the Maven Group ID changed to match. The one exception is jackson-annotations, which deliberately stays on the old com.fasterxml.jackson.annotation package and com.fasterxml.jackson.core Group ID so it can be shared between projects using Jackson 2 and Jackson 3 simultaneously.

Jackson 2 importJackson 3 import
com.fasterxml.jackson.databind.ObjectMappertools.jackson.databind.json.JsonMapper
com.fasterxml.jackson.databind.JsonNodetools.jackson.databind.JsonNode
com.fasterxml.jackson.core.JsonProcessingExceptiontools.jackson.core.JacksonException
com.fasterxml.jackson.databind.ser.std.StdSerializertools.jackson.databind.ser.std.StdSerializer
com.fasterxml.jackson.databind.deser.std.StdDeserializertools.jackson.databind.deser.std.StdDeserializer
com.fasterxml.jackson.annotation.*Unchanged — stays at com.fasterxml.jackson.annotation.*

In pom.xml, the Group ID changes for all modules except jackson-annotations:

<!-- Jackson 2 -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.17.2</version>
</dependency>

<!-- Jackson 3 -->
<dependency>
  <groupId>tools.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>3.1.2</version>
</dependency>

<!-- jackson-annotations: Group ID does NOT change -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>2.20.0</version>
</dependency>

A project-wide find-and-replace from com.fasterxml.jackson.databind to tools.jackson.databind handles most of the import changes, but do not touch the com.fasterxml.jackson.annotation imports — those are correct and unchanged. The OpenRewrite recipe org.openrewrite.java.jackson.UpgradeJackson_2_3 automates this across the entire codebase including transitive dependency updates:

<!-- Add to pom.xml build section to run the automated migration -->
<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <version>5.43.0</version>
  <configuration>
    <activeRecipes>
      <recipe>org.openrewrite.java.jackson.UpgradeJackson_2_3</recipe>
    </activeRecipes>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.openrewrite.recipe</groupId>
      <artifactId>rewrite-jackson</artifactId>
      <version>0.8.0</version>
    </dependency>
  </dependencies>
</plugin>

JacksonException Is Now Unchecked: A Silent Production Bug

This is the most dangerous behavioral change in Jackson 3 because it produces no compile error — it only fails at runtime. In Jackson 2, JsonProcessingException extended IOException, which meant any catch (IOException e) block also caught Jackson serialisation and deserialisation errors. In Jackson 3, JacksonException extends RuntimeException. Those catch blocks now silently stop catching Jackson exceptions.

// ── Jackson 2 ────────────────────────────────────────────────
// JsonProcessingException extends IOException — this catches Jackson errors
try {
    String json = mapper.writeValueAsString(order);
    sendToQueue(json);
} catch (IOException e) {              // <─ catches Jackson errors in 2.x
    log.error("Serialisation failed", e);
    throw new ServiceException(e);
}

// ── Jackson 3 ────────────────────────────────────────────────
// JacksonException extends RuntimeException — IOException no longer catches it
try {
    String json = mapper.writeValueAsString(order);
    sendToQueue(json);
} catch (IOException e) {              // <─ does NOT catch JacksonException in 3.x
    log.error("I/O error", e);
    throw new ServiceException(e);
}
// Jackson errors now propagate uncaught — potential production outage

// ── Correct Jackson 3 pattern ────────────────────────────────
try {
    String json = mapper.writeValueAsString(order);
    sendToQueue(json);
} catch (JacksonException e) {         // <─ explicit Jackson catch
    log.error("Serialisation failed", e);
    throw new ServiceException(e);
} catch (IOException e) {
    log.error("I/O error", e);
    throw new ServiceException(e);
}

Before upgrading, search your codebase for every catch (IOException e) block that wraps a Jackson call. Each one needs to be updated to catch JacksonException explicitly. The benefit is that Jackson 3 exceptions no longer need to be declared in method signatures, which cleans up code that uses Jackson inside lambdas and streams.

Mapper Construction: Mutable vs Builder

This is the most visible API change. Jackson 2 used a mutable ObjectMapper where configuration methods returned void or this. Jackson 3 introduces JsonMapper.builder(), which collects configuration and produces an immutable mapper on build().

// ── Jackson 2 ────────────────────────────────────────────────
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;

ObjectMapper mapper2 = new ObjectMapper()
    .registerModule(new JavaTimeModule())
    .registerModule(new Jdk8Module())
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .setSerializationInclusion(JsonInclude.Include.NON_NULL);
// mapper2 is still mutable — someone can call mapper2.disable(...) later

// ── Jackson 3 ────────────────────────────────────────────────
import tools.jackson.databind.json.JsonMapper;

JsonMapper mapper3 = JsonMapper.builder()
    // JavaTimeModule and Jdk8Module auto-registered — lines removed
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .serializationInclusion(JsonInclude.Include.NON_NULL)
    .build(); // immutable from this point forward

The Jackson 3 builder eliminates the bug class where a shared ObjectMapper is accidentally reconfigured after being injected into multiple components.

Custom Serializers and Deserializers: Class Renames

If your project has custom serializers or deserializers — and most non-trivial projects do — they will break with a compile error in Jackson 3. The base classes were renamed as part of the broader package and API cleanup.

Jackson 2 classJackson 3 class
JsonDeserializer<T>ValueDeserializer<T>
JsonSerializer<T>ValueSerializer<T>
StdDeserializer<T>StdDeserializer<T> (same name, new package: tools.jackson.*)
StdSerializer<T>StdSerializer<T> (same name, new package: tools.jackson.*)
DeserializationContextDeserializationContext (same name, new package)
JsonProcessingExceptionJacksonException
// ── Jackson 2 custom deserialiser ────────────────────────────
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;

public class MoneyDeserializer extends JsonDeserializer<Money> {
    @Override
    public Money deserialize(JsonParser p, DeserializationContext ctx)
            throws IOException, JsonProcessingException {
        return Money.of(p.getDecimalValue(), "GBP");
    }
}

// ── Jackson 3 custom deserialiser ────────────────────────────
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;

public class MoneyDeserializer extends ValueDeserializer<Money> {
    @Override
    public Money deserialize(JsonParser p, DeserializationContext ctx) {
        // throws clause removed — JacksonException is now unchecked
        return Money.of(p.getDecimalValue(), "GBP");
    }
}

Removed and Changed Default Features

Jackson 3 removed several MapperFeature settings that caused subtle, hard-to-diagnose bugs, and changed the defaults on others. These produce no compile error — they fail silently at runtime or change deserialization behavior.

FeatureJackson 2 behaviourJackson 3 behaviour
MapperFeature.AUTO_DETECT_CREATORSEnabled; detects single-arg constructors automaticallyRemoved — use @JsonCreator explicitly
MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORSEnabled by default; Jackson could overwrite final fields via reflectionDisabled — final fields are truly immutable
DeserializationFeature.FAIL_ON_TRAILING_TOKENSDisabled by defaultEnabled by default — adds small per-call validation overhead
MapperFeature.DEFAULT_VIEW_INCLUSIONEnabled by defaultChanged — review @JsonView behaviour if used

The AUTO_DETECT_CREATORS removal is the most likely to cause silent deserialization failures. Any class relying on a single-argument constructor being detected without an annotation will quietly fail. Add @JsonCreator to every constructor or factory method you intend Jackson to use:

// ── Jackson 2: constructor auto-detected ─────────────────────
public class OrderId {
    private final String value;
    public OrderId(String value) {   // auto-detected as creator in Jackson 2
        this.value = value;
    }
}

// ── Jackson 3: explicit annotation required ───────────────────
public class OrderId {
    private final String value;
    @JsonCreator                      // must be explicit in Jackson 3
    public OrderId(String value) {
        this.value = value;
    }
}

Performance: What Actually Changed and How to Tune It

The honest answer is that Jackson 3 performance is comparable to Jackson 2 for most workloads, but there are two specific configuration changes that can make Jackson 3 measurably slower out of the box if not addressed.

First, Jackson 3 changed the default RecyclerPool from a thread-local pool (fast for single-threaded or low-concurrency workloads) to a deque-based pool (better for high-concurrency but adds overhead in common cases). If your service is latency-sensitive or you observe a throughput regression after upgrading, restore the 2.x pool explicitly:

// ── Jackson 3: restore 2.x-equivalent RecyclerPool ───────────
import tools.jackson.core.JsonFactory;
import tools.jackson.core.util.JsonRecyclerPools;
import tools.jackson.databind.json.JsonMapper;

JsonFactory factory = JsonFactory.builder()
    .recyclerPool(JsonRecyclerPools.threadLocalPool())  // matches 2.x default
    .build();

JsonMapper mapper = JsonMapper.builder(factory)
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .build();

// For highly concurrent services, benchmark both:
// JsonRecyclerPools.threadLocalPool()  — best for low-to-medium concurrency
// JsonRecyclerPools.newConcurrentDequePool()  — default in 3.x, better for high concurrency

Second, DeserializationFeature.FAIL_ON_TRAILING_TOKENS is enabled by default in Jackson 3 and was off in Jackson 2. It adds a parser validation step after every successful parse. For high-throughput deserialisation of trusted internal payloads, you can disable it:

JsonMapper mapper = JsonMapper.builder()
    // Disable if you trust your input and need maximum throughput
    .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
    .build();
// Leave it enabled (default) for any service that accepts external JSON input

Beyond these two settings, Jackson 3 incorporated ideas from the Afterburner module — which used bytecode generation to bypass reflection overhead — directly into the core serialisation engine. For complex POJOs with many fields, Jackson 3 is generally at parity or slightly faster than Jackson 2 once the pool is configured correctly.

Java Records: Module Required vs Native

Jackson 2 required an explicit extra module and a -parameters compiler flag to deserialise records reliably. Jackson 3 eliminates both requirements — the canonical constructor is detected automatically via the RecordComponent reflection API introduced in Java 16.

// ── Jackson 2 ────────────────────────────────────────────────
// pom.xml needed: jackson-module-parameter-names
// javac needed: -parameters flag
ObjectMapper mapper2 = new ObjectMapper()
    .registerModule(new ParameterNamesModule());

public record ProductDto(Long id, String name, double price) {}

// Works, but requires the module and compiler flag

// ── Jackson 3 ────────────────────────────────────────────────
// No extra dependency, no compiler flag, no annotation
JsonMapper mapper3 = JsonMapper.builder().build();

public record ProductDto(Long id, String name, double price) {}

ProductDto dto = new ProductDto(1L, "Keyboard", 79.99);
String json = mapper3.writeValueAsString(dto);
// {"id":1,"name":"Keyboard","price":79.99}

ProductDto restored = mapper3.readValue(json, ProductDto.class);
System.out.println(restored.name()); // Keyboard

Optional<T>: Module vs Core

Optional<T> fields required the separate jackson-datatype-jdk8 artifact and an explicit registerModule(new Jdk8Module()) call in Jackson 2. Jackson 3 absorbs this support directly into jackson-databind — no separate dependency or registration step needed.

// ── Jackson 2 ────────────────────────────────────────────────
// pom.xml needed: jackson-datatype-jdk8
ObjectMapper mapper2 = new ObjectMapper()
    .registerModule(new Jdk8Module());

public class UserProfile {
    private String name;
    private Optional<String> nickname; // requires Jdk8Module
}

// ── Jackson 3 ────────────────────────────────────────────────
// No extra dependency, no module registration needed
JsonMapper mapper3 = JsonMapper.builder().build();

public class UserProfile {
    private String name;
    private Optional<String> nickname; // works natively
}

UserProfile p = new UserProfile("Alice", Optional.of("Ali"));
System.out.println(mapper3.writeValueAsString(p));
// {"name":"Alice","nickname":"Ali"}

Date/Time: Explicit Registration vs Auto-Registration

In Jackson 2, using any java.time.* type required adding jackson-datatype-jsr310 as a dependency and calling registerModule(new JavaTimeModule()) on every mapper instance. Jackson 3 bundles the equivalent support in its core and auto-registers it, so dates work immediately with no additional setup.

// ── Jackson 2 ────────────────────────────────────────────────
// pom.xml needed: jackson-datatype-jsr310
ObjectMapper mapper2 = new ObjectMapper()
    .registerModule(new JavaTimeModule())
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

// ── Jackson 3 ────────────────────────────────────────────────
// jackson-datatype-jsr310 not needed separately
// JavaTimeModule is auto-registered inside jackson-databind 3.x
JsonMapper mapper3 = JsonMapper.builder()
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .build();

// LocalDate, Instant, ZonedDateTime all work without any registration
public record EventDto(String title, LocalDate eventDate) {}

EventDto event = new EventDto("Conference", LocalDate.of(2026, 9, 15));
System.out.println(mapper3.writeValueAsString(event));
// {"title":"Conference","eventDate":"2026-09-15"}

Security: enableDefaultTyping() Gone in Jackson 3

The most security-significant change. enableDefaultTyping() was the root cause of the majority of Jackson CVEs — it allowed attackers to trigger arbitrary class instantiation by embedding a type identifier in JSON payloads. CVE-2019-14379, CVE-2019-14540, and CVE-2020-8840 are all variants of this class of attack. Jackson 3 removes the method entirely.

// ── Jackson 2 ────────────────────────────────────────────────
// Compiles, but dangerous. Deprecated in 2.10, removed in 2.16.
mapper2.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // <─ CVE risk

// ── Jackson 3 ────────────────────────────────────────────────
// Does not exist — this line will not compile.
// mapper3.enableDefaultTyping(...); // compile error: cannot find symbol

// Safe replacement in both versions:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = CardPayment.class,  name = "card"),
    @JsonSubTypes.Type(value = BankTransfer.class, name = "bank")
})
public abstract class Payment {}
// Only the explicitly listed classes can ever be deserialised

Sealed Classes: Jackson 2 Workaround vs Jackson 3 Native

Sealed classes function in Jackson 2 but require @JsonSubTypes to be manually maintained in parallel with the permits clause — a duplication that causes drift bugs when new subtypes are added. Jackson 3 can introspect the sealed hierarchy directly and auto-discover permitted types when each subtype carries a @JsonTypeName annotation, eliminating the parallel registry.

// ── Jackson 2 ────────────────────────────────────────────────
// Sealed classes work but @JsonSubTypes must be manually kept in sync
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "shapeType")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Circle.class,    name = "circle"),
    @JsonSubTypes.Type(value = Rectangle.class, name = "rectangle")
})
public sealed interface Shape permits Circle, Rectangle {}

// ── Jackson 3 ────────────────────────────────────────────────
// Jackson 3 can introspect sealed interfaces and auto-discover permitted types
// when @JsonTypeInfo is present — reducing boilerplate
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "shapeType")
public sealed interface Shape permits Circle, Rectangle {}
// @JsonSubTypes can be omitted when permitted types have @JsonTypeName

@JsonTypeName("circle")
public record Circle(double radius) implements Shape {}

@JsonTypeName("rectangle")
public record Rectangle(double width, double height) implements Shape {}

Jackson 3 in Spring Boot 4

Most developers encounter Jackson 3 through Spring Boot 4, which ships it as the default JSON library. There are a few Spring-specific details worth knowing before upgrading.

Spring Boot 4 auto-configures a JsonMapper bean. Several Spring classes were renamed to match the Jackson 3 API:

Spring Boot 3 classSpring Boot 4 class
Jackson2ObjectMapperBuilderCustomizerJsonMapperBuilderCustomizer
@JsonComponent@JacksonComponent
Jackson2ObjectMapperBuilderJsonMapperBuilder
JsonObjectSerializerObjectValueSerializer

If you need a phased migration — for example, because a third-party library still depends on Jackson 2 — Spring Boot 4 supports running both versions on the same classpath. The auto-configured JsonMapper uses Jackson 3; a manually configured ObjectMapper bean uses Jackson 2. The spring.jackson.use-jackson2-defaults: true property also lets you keep Jackson 2 behavioral defaults (such as the old trailing-token behaviour) while running the Jackson 3 library, giving you time to validate before fully committing to 3.x defaults.

// ── Spring Boot 3 Jackson customizer ─────────────────────────
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;

@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
    return builder -> builder
        .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

// ── Spring Boot 4 Jackson customizer ─────────────────────────
import org.springframework.boot.jackson.JsonMapperBuilderCustomizer;

@Bean
public JsonMapperBuilderCustomizer customizer() {
    return builder -> builder
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

Dependency Footprint: Jackson 2 vs Jackson 3

DependencyJackson 2 Needed?Jackson 3 Needed?
jackson-databindYes (com.fasterxml.jackson.core)Yes (tools.jackson.core)
jackson-coreTransitiveTransitive
jackson-annotationsTransitive (com.fasterxml.jackson.core)Transitive — stays at com.fasterxml.jackson.core
jackson-datatype-jsr310Yes (for java.time.*)No — built in
jackson-datatype-jdk8Yes (for Optional)No — built in
jackson-module-parameter-namesYes (for records, reliably)No — native

API Method Reference: Jackson 2 → Jackson 3 Equivalents

Jackson 2 method / classJackson 3 equivalent
new ObjectMapper()JsonMapper.builder().build()
mapper.registerModule(m)JsonMapper.builder().addModule(m).build()
mapper.configure(Feature.X, true)builder.enable(Feature.X)
mapper.setSerializationInclusion(NON_NULL)builder.serializationInclusion(NON_NULL)
mapper.enableDefaultTyping(...)Removed — use @JsonTypeInfo
mapper.copy()Use builder pattern; mappers are immutable
new ObjectMapper(factory)JsonMapper.builder(streamFactory).build()
JsonDeserializer<T>ValueDeserializer<T>
JsonSerializer<T>ValueSerializer<T>
JsonProcessingExceptionJacksonException (unchecked)
catch (IOException e) wrapping Jackson callcatch (JacksonException e)

When to Upgrade: Jackson 2 vs Jackson 3

ScenarioRecommendation
New greenfield project on Java 17+Use Jackson 3.1.2 from the start
Upgrading to Spring Boot 4Jackson 3 comes with Spring Boot 4 — plan for package renames and exception catch updates
Existing Spring Boot 3.x project (staying on 3.x)Optional upgrade — override managed version, update config class and imports
Project on Java 8 or Java 11Stay on Jackson 2.17.x — Jackson 3 requires Java 17 for all core modules
Security-sensitive service with polymorphic typesPrioritise upgrade to get compile-time enforcement of safe typing
Library or SDK targeting wide Java compatibilityEvaluate carefully — publishing a Jackson 3 library forces all consumers to Java 17+
Project with many custom serializers/deserializersAllow extra migration time for ValueDeserializer / ValueSerializer renames

See Also

AI Prompts for Jackson 3 vs Jackson 2 Comparison

Diff My Config: Jackson 2 vs 3

Here is my current Jackson 2 @Configuration class: [paste class here]. Show me the exact before-and-after diff of migrating it to Jackson 3.1.2. For every changed line, add an inline comment explaining what changed and why — for example, why registerModule(new JavaTimeModule()) is removed, why new ObjectMapper() becomes JsonMapper.builder().build(), and what the builder equivalent is for each setter-style call. Highlight any line that would cause a compile error in Jackson 3.

What it does: Produces a side-by-side diff of your exact configuration with every changed line annotated inline, clearly marking compile-breaking changes versus safe simplifications so you can prioritise which changes are mandatory versus optional before starting the upgrade.

When to use it: At the start of a Jackson 3 upgrade when you want to scope the exact code changes before committing, or when preparing a pull request description that explains every configuration change to reviewers who are unfamiliar with the Jackson 3 API differences.

Which Jackson 3 Features Help My Model

Here are my main domain and DTO classes: [paste classes here]. Analyse them and tell me where Jackson 3’s improvements would simplify or remove existing boilerplate. Look for: POJOs with only final fields that could become records, Optional fields that currently require Jdk8Module, date fields that currently require explicit JavaTimeModule registration, and abstract class hierarchies that could use sealed interfaces. For each finding, show the before class and the simplified Jackson 3 version side by side.

What it does: Scans the provided classes for patterns Jackson 3 handles natively, produces a before-and-after for each affected class showing the removed boilerplate, and estimates how many lines of configuration and module registration can be deleted — giving a code-level answer to “is the upgrade worth it?”

When to use it: When evaluating Jackson 3 adoption ROI for a specific service before committing to the migration, or when making the case to a team that the upgrade has tangible simplification benefits beyond just a version number bump.

Audit IOException Catch Blocks for Jackson 3 Safety

I am upgrading from Jackson 2 to Jackson 3. In Jackson 3, JacksonException extends RuntimeException instead of IOException. Search my codebase for every catch (IOException e) block that wraps a Jackson serialisation or deserialisation call — writeValueAsString, readValue, writeValue, or any other mapper method. For each one found: show the existing catch block, explain whether it will silently stop catching Jackson errors in Jackson 3, and show the corrected version with an explicit catch (JacksonException e) added. Group findings by risk level: silent failure (catch block misses the exception entirely) vs. safe (already catching a broad enough exception type).

What it does: Identifies every catch block in the codebase that will silently stop working after the Jackson 3 upgrade, grouped by risk level, with corrected code for each one — preventing production outages caused by uncaught JacksonException after the version bump.

When to use it: Before merging a Jackson 3 upgrade PR, as a final safety check. The unchecked exception change is the most likely source of silent runtime regressions and is easy to miss in a code review focused on import changes and API renames.

Write a Cross-Version Compatibility Test Suite

I need confidence that my Jackson 3.1.2 setup produces identical JSON output to my Jackson 2.17.2 setup. Generate a JUnit 5 test class covering: serialisation of a POJO with standard fields, a Java record, a class with Optional fields, LocalDate and Instant fields, deserialisation with unknown fields in the JSON, and deserialisation of a generic List wrapper. Each test should assert the exact JSON string and the exact deserialised field values. Flag any scenario where the output differs between versions.

What it does: Generates a JUnit 5 test class with one method per scenario, each asserting exact JSON string output and exact field values after deserialisation — covering the six most common compatibility failure modes so you have a runnable regression suite to execute before and after the version switch.

When to use it: When doing a phased upgrade where the Jackson 3 mapper must produce byte-identical JSON to the Jackson 2 mapper — for example when serialised output is cached, signed, or consumed by downstream systems that would break on any field order or date format change.

FAQs

Is Jackson 3 backward compatible with Jackson 2 at the serialised JSON level?

Yes — the JSON wire format is identical for standard types. A service upgraded to Jackson 3 will produce the same JSON as Jackson 2 for POJOs, records, collections, and dates, so downstream consumers are unaffected.

Are Jackson 2 and Jackson 3 binary compatible at the Java API level?

No. The package namespace changed from com.fasterxml.jackson.* to tools.jackson.*, several APIs were removed (enableDefaultTyping(), AUTO_DETECT_CREATORS), base classes were renamed (JsonDeserializer → ValueDeserializer), and the exception hierarchy changed. Code written for Jackson 2 will not compile against Jackson 3 without changes.

What Java version does Jackson 3 require?

Jackson 3 requires Java 17 for all core modules (jackson-databind, jackson-core). The jackson-annotations module is an exception — it remains on the 2.x line and does not require Java 17, allowing it to be shared across Jackson 2 and Jackson 3 projects. If your project is on Java 8 or Java 11, you must stay on Jackson 2.x.

Does Jackson 3 improve JSON serialisation performance?

For most workloads, Jackson 3 performance is comparable to Jackson 2 once configured correctly. However, two default changes in Jackson 3 can cause measurable regressions if not addressed: the switch from a thread-local RecyclerPool to a deque-based pool, and the enabling of FAIL_ON_TRAILING_TOKENS by default. Both can be tuned back to 2.x behaviour — see the Performance section above for the exact code. On the positive side, Jackson 3 incorporates Afterburner-style optimisations into its core, which benefits POJO serialisation with many fields.

Can Jackson 3 and Jackson 2 be on the same classpath?

Yes, and unlike most conflicting libraries, Jackson 3 and Jackson 2 can coexist on the same classpath because they now use different package namespaces (tools.jackson.* vs com.fasterxml.jackson.*). Spring Boot 4 uses this explicitly — its auto-configuration provides a Jackson 3 JsonMapper while allowing a manually configured Jackson 2 ObjectMapper bean to coexist for third-party libraries that have not yet migrated. Upgrade one service at a time and isolate each service’s primary mapper.

Will my @JsonProperty, @JsonIgnore, and other annotations still work?

Yes. All @Json* annotations come from jackson-annotations, which intentionally stays on the com.fasterxml.jackson.annotation package and is shared between Jackson 2 and Jackson 3. No annotation imports need to change during the migration.

Conclusion

Jackson 3 is a cleaner, safer, and more modern library than Jackson 2, but it is a genuine breaking upgrade — not a version bump. The key improvements are the immutable builder API, native support for records and Optional without extra modules, auto-registered date/time support, and the forced removal of the dangerous default typing methods. The changes that bite most teams are the package namespace rename (every import), the JacksonException becoming unchecked (every catch block around Jackson calls), the JsonDeserializer → ValueDeserializer rename (every custom serializer), and the Java 17 requirement. If your project runs on Java 17 or later, Jackson 3.1.2 is the right choice for all new development. For projects on Java 8 or 11, Jackson 2.17.x remains the supported path until a Java version upgrade is possible.

Further Reading

Leave a Reply

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