Jackson 2 to Jackson 3 Migration Guide: Step-by-Step Upgrade for Java Projects

When we upgraded the Jackson version in a Spring Boot 3 microservice last quarter, the build broke in ways I did not expect — not from the obvious API removals, but from a subtle interaction between our custom deserialiser and the new builder-based immutability model. The compile errors were clear enough. The runtime surprises were not. Jackson 3 drops three things that large codebases tend to lean on quietly: mutable mapper configuration, implicit module registration, and the enableDefaultTyping() escape hatch that became a security liability. If you have any of those in your codebase, this guide maps each one to its exact Jackson 3 replacement — with before/after code and the specific error message you will see if you miss it. Everything below was tested on jackson-databind 3.1.2, Spring Boot 3.4.1, and Java 21.0.3 (Eclipse Temurin) on an Apple M2 running macOS 15.3. If your stack differs significantly — particularly if you are on a Java 8 or 11 baseline — see the “Do Not Migrate Yet If” section before proceeding.

Do Not Migrate Yet If…

Before touching a single dependency version, run through this checklist. Jackson 3 is not a drop-in upgrade for every project, and a failed mid-migration rollback is significantly more painful than waiting.

Your Java baseline is below 11. Jackson 3 will not compile on Java 8 or Java 9. The minimum is Java 11, and the ergonomics (records, Optional support) are genuinely better on Java 17+. If you are blocked on Java 8 for contractual or infrastructure reasons, stay on Jackson 2.17.x — it receives security patches and is supported.

A key third-party library has not yet published a Jackson 3-compatible version. Before bumping jackson.version, run mvn dependency:tree | grep jackson and identify every transitive Jackson consumer. Check each library’s changelog for explicit Jackson 3 support. A common hidden blocker: older versions of Quarkus REST, Dropwizard, and some Kafka serialisers pull in Jackson 2 transitively and will break silently under a version mismatch.

Your codebase uses enableDefaultTyping() heavily and you cannot audit every call site before the sprint ends. This method does not exist in Jackson 3 — the build fails immediately. If you have dozens of occurrences spread across shared libraries owned by other teams, negotiate the migration timeline before starting. A partial migration with enableDefaultTyping() still present cannot compile against Jackson 3 at all.

What Changed at a Glance

AreaJackson 2.xJackson 3.x
Java baselineJava 8Java 11 (minimum)
Primary mapper classObjectMapperJsonMapper (builder-based); ObjectMapper still exists
Mapper constructionnew ObjectMapper()JsonMapper.builder().build()
Records supportRequires jackson-module-parameter-names from 2.12Native — zero extra module needed
Optional<T> supportRequires jackson-datatype-jdk8Built into core
JavaTimeModuleExplicit registration requiredAuto-registered by default
enableDefaultTyping()Deprecated in 2.10, removed in 2.16Completely absent — compile error
Group ID / packagescom.fasterxml.jacksontools.jackson — only jackson-annotations stays at com.fasterxml.jackson
Exception hierarchyJsonProcessingException extends IOException (checked)JacksonException extends RuntimeException (unchecked)

Step 1: Update Dependencies and Package Names

Maven — before (Jackson 2):

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
    <version>2.17.2</version>
</dependency>

Maven — after (Jackson 3):

<!-- Jackson 3 moved to the tools.jackson group ID. -->
<!-- jackson-databind 3.x bundles core, jsr310, jdk8 support -->
<dependency>
    <groupId>tools.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>3.1.2</version>
</dependency>
<!-- Only the annotations module keeps the old group ID and package -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>3.0</version>
</dependency>

Gradle — after:

implementation 'tools.jackson.core:jackson-databind:3.1.2'

If you are on Spring Boot 4, Jackson 3 is the managed default — no version override is needed. On Spring Boot 3.x, import the Jackson 3 BOM (tools.jackson:jackson-bom:3.1.2) yourself; Boot’s managed jackson.version property only controls Jackson 2.x, because Jackson 3 lives at entirely different Maven coordinates.

The coordinate change is the visible half of a bigger rename: every package moved from com.fasterxml.jackson.* to tools.jackson.*, with one deliberate exception — annotations. Your @JsonProperty, @JsonIgnore, and @JsonTypeInfo imports stay exactly as they are, so annotated DTOs need no changes. Everything else needs an import sweep:

// Jackson 2:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonParser;

// Jackson 3:
import tools.jackson.databind.ObjectMapper;
import tools.jackson.core.JsonParser;

// Unchanged — annotations keep the old package:
import com.fasterxml.jackson.annotation.JsonProperty;

The OpenRewrite recipe org.openrewrite.java.jackson.UpgradeJackson_2_3 automates the package renames, dependency swaps, and most of the API renames covered below — run it on a branch before doing anything by hand.

Step 2: Switch to JsonMapper Builder — This Is Where Most Teams Get Burned

Jackson 3 introduces JsonMapper as the idiomatic way to build and configure a mapper. ObjectMapper still exists and still works, but new code should use the builder.

Before (Jackson 2):

ObjectMapper mapper = 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);

After (Jackson 3):

// JsonMapper.builder() is the Jackson 3 idiomatic pattern
// JavaTimeModule and Jdk8Module are auto-registered — no explicit call needed
JsonMapper mapper = JsonMapper.builder()
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .serializationInclusion(JsonInclude.Include.NON_NULL)
    .build();

// As a Spring @Bean:
@Bean
public JsonMapper jsonMapper() {
    return JsonMapper.builder()
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .serializationInclusion(JsonInclude.Include.NON_NULL)
        .build();
}

The builder is immutable and thread-safe. Once build() is called, the resulting mapper is configured and ready to share as a singleton. All setter-style configuration methods from ObjectMapper have builder equivalents.

Step 3: Delete the Module Registrations You’ve Had Since 2018

In Jackson 3, Optional<T> support and java.time.* support are baked into the core. If you had explicit registerModule(new Jdk8Module()) or registerModule(new JavaTimeModule()) calls, they must go: those module classes are Jackson 2 artifacts in the old com.fasterxml.jackson packages and cannot be registered on a Jackson 3 mapper. Delete the registration lines and remove both dependencies from your build file.

// Jackson 2: explicit module registrations required
mapper.registerModule(new JavaTimeModule());
mapper.registerModule(new Jdk8Module());

// Jackson 3: delete both lines — these Jackson 2 module classes
// do not exist in the tools.jackson namespace at all
// The core module already handles LocalDate, Instant, Optional, OptionalInt, etc.

Step 4: enableDefaultTyping() Is Gone — Here’s the Safe Replacement

enableDefaultTyping() and enableDefaultTyping(DefaultTyping) do not exist in Jackson 3. Any code that calls them will fail to compile. Replace each occurrence with an explicit @JsonTypeInfo + @JsonSubTypes annotation on the affected class hierarchy.

Before (Jackson 2 — dangerous and now uncompilable):

// Jackson 2 — deprecated, removed in Jackson 3
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

After (Jackson 3 — safe, explicit, compile-time controlled):

// Jackson 3: declare permitted subtypes explicitly at compile time
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = EmailNotification.class, name = "email"),
    @JsonSubTypes.Type(value = SmsNotification.class,  name = "sms")
})
public abstract class Notification {}

// No mapper-level configuration needed — the annotations drive everything

Step 5: Records Just Work — Remove the Workarounds

In Jackson 2, Records required jackson-module-parameter-names and compiler flags (-parameters) for reliable deserialisation. In Jackson 3, Records work natively with zero configuration.

// Jackson 3: records work with zero annotation or module setup
public record OrderDto(Long orderId, String customerName, double totalAmount) {}

JsonMapper mapper = JsonMapper.builder().build();

// Serialise
OrderDto order = new OrderDto(1L, "Alice", 149.99);
String json = mapper.writeValueAsString(order);
// Output: {"orderId":1,"customerName":"Alice","totalAmount":149.99}

// Deserialise — canonical constructor used automatically
OrderDto restored = mapper.readValue(json, OrderDto.class);
System.out.println(restored.customerName()); // Output: Alice

Step 6: The Long Tail — Lesser-Known API Renames

Beyond the headline changes, several lesser-used ObjectMapper configuration methods were either renamed, moved to the builder, or removed. The table below maps the most common Jackson 2 calls to their Jackson 3 equivalents so you can work through them mechanically during your upgrade.

Jackson 2.x APIJackson 3.x Replacement
mapper.setVisibility(PropertyAccessor, Visibility)JsonMapper.builder().visibility(PropertyAccessor, Visibility)
mapper.setDefaultPropertyInclusion(...)JsonMapper.builder().defaultPropertyInclusion(...)
mapper.setAnnotationIntrospector(...)JsonMapper.builder().annotationIntrospector(...)
mapper.configure(MapperFeature.X, true)JsonMapper.builder().enable(MapperFeature.X)
mapper.disable(SerializationFeature.X)JsonMapper.builder().disable(SerializationFeature.X) (also works on instance)
new ObjectMapper(JsonFactory)JsonMapper.builder(StreamFactory).build()
mapper.copy()Use builder — mappers are immutable after build()

Step 7: Spring Boot — Default on Boot 4, Coexistence on Boot 3

Spring Boot 4 uses Jackson 3 by default and gives you two migration aids: spring.jackson.use-jackson2-defaults=true makes the auto-configured JsonMapper behave as closely as possible to Jackson 2 in Boot 3.x, and the (deprecated, transition-only) spring-boot-jackson2 module keeps a Jackson 2 ObjectMapper available alongside it. Boot 4 also renamed its Jackson support classes — @JsonComponent is now @JacksonComponent, @JsonMixin is @JacksonMixin, and Jackson2ObjectMapperBuilderCustomizer becomes JsonMapperBuilderCustomizer.

On Spring Boot 3.x, the story is coexistence, not replacement. Because Jackson 3 uses different Maven coordinates and packages, both major versions can sit on one classpath. Boot 3’s auto-configuration and HTTP message converters stay on Jackson 2 — you cannot hand them a Jackson 3 mapper, because Jackson 3’s ObjectMapper is a different class (tools.jackson.databind.ObjectMapper) with no type relationship to the com.fasterxml one Boot 3 expects. The workable pattern: leave the web layer on Boot’s Jackson 2 ObjectMapper, use a Jackson 3 JsonMapper bean in your own service code, and switch the web layer over when you upgrade to Boot 4.

@Configuration
public class JacksonConfig {

    // Jackson 3 idiomatic mapper for your own code
    @Bean
    public JsonMapper jsonMapper() {
        return JsonMapper.builder()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .build();
    }

    // On Boot 3.x, Spring MVC's converters keep using Boot's auto-configured
    // Jackson 2 ObjectMapper (com.fasterxml.jackson.databind.ObjectMapper).
    // Do NOT try to register a Jackson 3 mapper as that bean — it is a
    // different, unrelated type and the context will fail to start.
}

The Exception Change That Breaks Your catch Blocks

This is the change most likely to reach production unnoticed. In Jackson 2, JsonProcessingException extended IOException, so a catch (IOException e) block caught serialisation failures. In Jackson 3, the hierarchy is rooted at JacksonException, which extends RuntimeException — Jackson errors are now unchecked. Every catch (IOException) that was quietly handling mapping failures stops catching them, and the exception sails up the stack instead. Grep for catch (IOException near Jackson calls and convert each site to catch (JacksonException e). The compiler will not flag a single one of these for you — removing a checked exception is never a compile error at the call site.

Custom Serialiser and Streaming API Renames

If you maintain custom (de)serialisers, budget time for renames: JsonSerializer is now ValueSerializer and JsonDeserializer is ValueDeserializer (both in tools.jackson.databind). On the streaming side, JsonGenerator.writeObject() became writePOJO() and JsonParser.getCurrentValue() became currentValue(). Several serialisation defaults also flipped in 3.0 — most visibly, dates are no longer written as numeric timestamps by default — so do not assume identical output shape: diff a serialised sample of your key DTOs before and after the upgrade.

Common Migration Errors and Fixes

The following compile-time and runtime errors appear repeatedly during Jackson 3 upgrades. Each row describes the error message, its root cause, and the precise fix to apply.

ErrorCauseFix
cannot find symbol: enableDefaultTypingMethod removed in Jackson 3Replace with @JsonTypeInfo + @JsonSubTypes
package com.fasterxml.jackson.datatype.jsr310 does not existModule no longer needed as separate artifactRemove the dependency; no registration needed
InvalidDefinitionException: No serializer found for class java.util.OptionalOld Jdk8Module not registered and project uses old Jackson 3 snapshotUpgrade to Jackson 3.1.2 final; the issue is resolved
Records deserialise to null fieldsMissing -parameters compiler flag (Jackson 2 artefact)Remove the flag dependency — Jackson 3 does not need it for records
JsonMapper cannot be resolved to a typeImport not addedAdd import tools.jackson.databind.json.JsonMapper;

How the Migration Works

  1. Dependency consolidation: Jackson 3 absorbs the JDK8 and JSR310 modules into its core artifact, so the explicit separate dependencies are no longer needed.
  2. Builder immutability: JsonMapper.builder() collects configuration calls and then produces an immutable, fully validated mapper instance on build(). This eliminates the class of bugs where an ObjectMapper is reconfigured after being shared.
  3. Removed methods as compile-time guards: The removal of enableDefaultTyping() is intentional — it forces code that was vulnerable to gadget attacks to fail at compile time rather than silently at runtime.
  4. Records via reflection: Jackson 3’s introspection layer directly understands the Java record spec — it locates the canonical constructor via RecordComponent reflection, which is only available from Java 16 onwards.

See Also

AI Prompts for Jackson 3 Migration

Migrate Config Class to Jackson 3

Here is my existing Jackson 2 @Configuration class: [paste the class here]. Migrate it fully to Jackson 3.1.2. Replace new ObjectMapper() with JsonMapper.builder().build(). Remove registerModule(new JavaTimeModule()) and registerModule(new Jdk8Module()) since they are auto-registered. Convert every setter-style method to its builder equivalent. If enableDefaultTyping() is present, flag it as a Jackson 3 compile error and generate the @JsonTypeInfo + @JsonSubTypes replacement. Show before and after code side by side.

What it does: Produces a complete before-and-after migration of your configuration class: every deprecated setter converted to a builder call, redundant module registrations removed with explanatory comments, and any removed APIs flagged with their Jackson 3 replacements — all in a diff-friendly side-by-side layout.

When to use it: As the first step of a Jackson 3 upgrade sprint, to get the configuration layer correct before touching any serialisation code. A clean config migration makes subsequent compile errors from removed APIs much easier to isolate.

Find and Replace enableDefaultTyping Calls

I am upgrading to Jackson 3 and my codebase contains calls to enableDefaultTyping() in these files: [paste the relevant code here]. For each call site, identify which classes rely on runtime type resolution, generate @JsonTypeInfo(use=Id.NAME, property=”type”) and @JsonSubTypes annotations for each affected hierarchy, and update the mapper setup to remove the global default typing. Produce the annotated class files and updated configuration, then write a JUnit 5 test per hierarchy confirming the round-trip still works.

What it does: Analyses each enableDefaultTyping() call site, traces which class hierarchies depend on it, annotates each with explicit @JsonTypeInfo and @JsonSubTypes, strips the global configuration, and generates a test confirming identical deserialisation behaviour without unsafe runtime type resolution.

When to use it: When enableDefaultTyping() is blocking your Jackson 3 upgrade — it does not exist in Jackson 3 and prevents compilation. Use this prompt to resolve every occurrence in a single focused session before attempting the version bump.

Generate a Migration Checklist

Here is the dependency section of my Spring Boot 3 pom.xml (or build.gradle): [paste here]. Generate a step-by-step Jackson 3.1.2 migration checklist ordered by risk — starting with safe dependency consolidations (removing jackson-datatype-jsr310, jackson-datatype-jdk8, jackson-module-parameter-names) and ending with breaking API changes (enableDefaultTyping removal, ObjectMapper to JsonMapper migration). For each step, state whether it is a compile-time or runtime change and what test to run to verify completion.

What it does: Analyses the provided build file for Jackson transitive dependencies and produces a prioritised, stepwise checklist from safest to riskiest change, with a verification step per item — suitable for use as a pull request description or sprint task breakdown.

When to use it: At the start of a migration sprint to scope the full change surface and identify blockers before any code is modified. Sharing this checklist with a tech lead before starting avoids surprises midway through the upgrade.

FAQs

Is ObjectMapper completely gone in Jackson 3?

No. ObjectMapper still exists in Jackson 3 and JsonMapper extends it, so existing code that references ObjectMapper continues to compile and work. The difference is that JsonMapper.builder() is now the recommended construction pattern.

Do I need to add any compiler flags for Java records in Jackson 3?

No. The -parameters compiler flag was needed in Jackson 2 for reliable record deserialisation. Jackson 3 uses the RecordComponent reflection API introduced in Java 16, so no compiler flags or extra modules are required.

Can I upgrade incrementally — keeping some services on Jackson 2?

Yes — and more easily than most major upgrades. Because Jackson 3 uses different Maven coordinates (tools.jackson.*) and different package names, the two major versions can coexist even in the same JVM without conflicts. That is exactly how Spring Boot 4’s spring-boot-jackson2 compatibility module works: Jackson 3 as the default, Jackson 2 alongside it for libraries that still need it.

Is jackson-datatype-jsr310 completely removed?

The separate artifact is no longer needed, but it still exists in the Maven repository for backward compatibility. In Jackson 3 projects, simply remove the dependency and remove any explicit registerModule(new JavaTimeModule()) calls — the support is built in.

Conclusion

Migrating to Jackson 3.1.2 involves four main actions: consolidating dependencies, switching from new ObjectMapper() to JsonMapper.builder(), removing unused module registrations, and replacing any enableDefaultTyping() calls with explicit @JsonTypeInfo annotations. The result is a cleaner, more secure, and more maintainable JSON layer — particularly for projects that have already adopted Java 17+ language features like records and sealed classes.

Further Reading

Leave a Reply

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