Jackson 3 Migration: Upgrade From Jackson 2 to Jackson 3

Jackson 3 moves to the tools.jackson group id and packages, makes JsonMapper immutable and replaces the checked JsonProcessingException with JacksonException. This guide shows each change with a short snippet, the changed defaults side by side, how Spring Boot 4 uses Jackson 3, and a migration checklist.

Mapping of Jackson 2 Maven coordinates and Java packages to their Jackson 3 equivalents, with jackson-annotations unchanged and the Java 8 modules built in

Jackson 3 is the new major version of the Jackson JSON library, with new Maven coordinates and package names under tools.jackson. A Jackson 3 migration means changing our dependencies and imports from com.fasterxml.jackson to tools.jackson. It also changes our JSON output, because several defaults are different.

Most apps meet Jackson 3 in an upgrade to Spring Boot 4, which uses it by default, or when a library they use moves to it. Jackson 3 needs Java 17 or later.

The following example writes a Recipe record to JSON and reads it back with Jackson 3.2.3 on Java 25. The before snippets use Jackson 2.22.3, and the example project runs both versions side by side, plus a Spring Boot 4.1.1 app. The JsonMapper class comes from the tools.jackson.databind.json package.

JsonMapper mapper = JsonMapper.builder().build();         // immutable, java.time support built in
String json = mapper.writeValueAsString(new Recipe("pancakes", 4, LocalDate.of(2026, 10, 5)));
                                                          // {"name":"pancakes","servings":4,"createdOn":"2026-10-05"}
Recipe recipe = mapper.readValue(json, Recipe.class);     // no checked exception, recipe.servings() = 4

We don’t register JavaTimeModule, and we need no try-catch block. The date comes out as the text “2026-10-05”, which is one of the new defaults.

Each change below gets a short before/after snippet, followed by the Spring Boot 4 setup and a checklist.

1. What Changes From Jackson 2 to Jackson 3

Jackson 3.0 was released on October 3, 2025, and it removes everything that was deprecated in Jackson 2.20. Most changes are renames. The new defaults change the JSON output, so they need the most testing.

ChangeJackson 2.xJackson 3.xWhat we do
Maven group idcom.fasterxml.jackson.coretools.jackson.coreChange the coordinates (section 2)
Java packagescom.fasterxml.jackson.*tools.jackson.*Change the imports
Annotationscom.fasterxml.jackson.annotationUnchanged (2.x artifact)Keep the annotation imports
Minimum JavaJava 8Java 17Upgrade the JDK first
ObjectMapperChanged with setters after creationImmutable, built once with JsonMapper.builder()Move setters into the builder (section 3)
Base exceptionJsonProcessingException (checked)JacksonException (unchecked)Update catch blocks (section 4)
java.time, Optional, parameter namesSeparate modulesBuilt into jackson-databindRemove the modules (section 5)
DefaultsDates as timestamps, unknown properties failISO dates, unknown properties ignored, properties sortedCheck the JSON output (section 6)
SerializersJsonSerializer, SerializerProviderValueSerializer, SerializationContextRename the classes (section 7)
Spring BootSpring Boot 3.x uses Jackson 2Spring Boot 4.x uses Jackson 3Rename customizers and properties (section 8)

Jackson 3.0 is not a long-term support (LTS) release. Jackson 3.1 is the first LTS release of the 3.x line, so we use 3.1 or the newer 3.2.

2. New Maven Coordinates and Java Packages

Jackson 3 moves all Jackson modules to the new Maven group id tools.jackson and to tools.jackson.* packages. The one exception is jackson-annotations, which keeps the com.fasterxml.jackson.annotation package and a 2.x version, so Jackson 2 and Jackson 3 share annotations such as @JsonProperty. Annotations that come from jackson-databind, such as @JsonSerialize, do move to tools.jackson.databind.annotation.

Jackson 2 Maven coordinates and Java packages next to their Jackson 3 names, with jackson-annotations unchanged and three modules built into jackson-databind
Everything moves to tools.jackson except jackson-annotations, and the three modules for java.time, Optional and parameter names are no longer needed.

In the pom.xml, we replace the com.fasterxml.jackson.core dependency with the tools.jackson.core one and delete jackson-datatype-jsr310. We also import the Jackson 3 jackson-bom, a list of matching versions for all Jackson modules, so our Jackson dependencies need no version.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>tools.jackson</groupId>
      <artifactId>jackson-bom</artifactId>
      <version>3.2.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>tools.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
  </dependency>
</dependencies>

Because the package names differ, one app can use Jackson 2 and Jackson 3 together, for example while some libraries still need Jackson 2. Each version only sees its own classes, so a Jackson 2 serializer has no effect on a Jackson 3 JsonMapper.

3. Building an Immutable JsonMapper

In Jackson 2, we create an ObjectMapper and change it later with methods such as enable() or setSerializationInclusion(). In Jackson 3, a mapper is immutable, so it cannot change after we build it. We pass every setting to a builder once, so no code can change a shared mapper later.

ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
JsonMapper mapper = JsonMapper.builder()
    .enable(SerializationFeature.INDENT_OUTPUT)
    .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
    .build();

JsonMapper compact = mapper.rebuild()                     // builder with the settings of mapper
    .disable(SerializationFeature.INDENT_OUTPUT)
    .build();                                             // new instance, mapper is unchanged

The method rebuild() replaces ObjectMapper.copy(), which Jackson 3 removed. Jackson 3 also has one mapper class per format. So new ObjectMapper(new YAMLFactory()) becomes a YAMLMapper, and for JSON we use JsonMapper.

4. JacksonException Replaces the Checked JsonProcessingException

In Jackson 2, JsonProcessingException extends IOException, so every readValue() and writeValueAsString() call needs a try-catch block or a throws clause. In Jackson 3, the base exception is JacksonException, which extends RuntimeException, so we can call Jackson inside lambdas and streams without a try-catch.

Bad JSON from a client or a file still throws, so we keep a catch block wherever we read untrusted data. Only the exception type in the catch changes.

Optional<Recipe> recipe;
try {
  recipe = Optional.of(mapper.readValue("{bad json", Recipe.class));
} catch (JacksonException e) {                            // was JsonProcessingException
  recipe = Optional.empty();                              // Optional.empty
}

The subtypes have new names as well, so every catch of a specific Jackson 2 exception needs the new type.

Jackson 2.xJackson 3.x
JsonMappingExceptionDatabindException
JsonParseExceptionStreamReadException
JsonGenerationExceptionStreamWriteException

5. Java Time and Optional Support Built In

Jackson 2 needs the jackson-datatype-jsr310 module for LocalDate and the other java.time types. Without it, Jackson 2 throws the Java 8 date/time type error. Jackson 3 builds this module into jackson-databind, together with jackson-datatype-jdk8 (for Optional) and jackson-module-parameter-names (for constructor parameter names). So we delete the three dependencies and their registerModule() calls.

JsonMapper mapper = JsonMapper.builder().build();
String date = mapper.writeValueAsString(LocalDate.of(2026, 10, 5));   // "2026-10-05"
String fruit = mapper.writeValueAsString(Optional.of("apple"));       // "apple"

6. Changed Defaults in Jackson 3

Jackson 3 changes several defaults that many teams changed by hand in Jackson 2. Each row shows the result with no extra configuration (Jackson 2 with JavaTimeModule).

Feature (Jackson 3 enum)InputJackson 2.xJackson 3.x
DateTimeFeature.WRITE_DATES_AS_TIMESTAMPSLocalDate.of(2026, 10, 5)[2026,10,5]“2026-10-05”
DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPSDuration.ofMinutes(25)1500.000000000“PT25M”
MapperFeature.SORT_PROPERTIES_ALPHABETICALLYClass with fields name, grams, veganname, grams, vegangrams, name, vegan
EnumFeature.WRITE_ENUMS_USING_TO_STRINGEASY with toString() returning “easy”“EASY”“easy”
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIESJSON with an extra rating fieldUnrecognizedPropertyExceptionField ignored
DeserializationFeature.FAIL_ON_TRAILING_TOKENSTwo JSON objects in one stringFirst object readMismatchedInputException
DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES{“grams”:null} into an intgrams = 0MismatchedInputException
SerializationFeature.FAIL_ON_EMPTY_BEANSClass without propertiesInvalidDefinitionException{}
MapperFeature.DEFAULT_VIEW_INCLUSION@JsonView active, author field without a viewstars and authorstars only

The sorting does not apply to properties set through the constructor, so a record keeps its field order. Tests that compare JSON as plain strings are the first thing that breaks after the upgrade, so we compare JSON while ignoring property order, or update the expected strings.

To restore one old setting, we turn that feature on or off in the builder. Date settings are in the new DateTimeFeature enum, covered in the Jackson dates guide. To restore most Jackson 2 defaults at once, we start from JsonMapper.builderWithJackson2Defaults().

JsonMapper strict = JsonMapper.builder()
    .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
    .build();
String date = strict.writeValueAsString(LocalDate.of(2026, 10, 5));   // [2026,10,5]

JsonMapper legacy = JsonMapper.builderWithJackson2Defaults().build();
String dish = legacy.writeValueAsString(new Dish("pancakes", Difficulty.EASY));
                                                          // {"name":"pancakes","difficulty":"EASY"}

7. Renamed Classes and Methods

Jackson 3 renames many core types. Types that work for every format, not only JSON, lose the “Json” at the start of their name. We meet the new names most often in custom serializers and JsonNode code.

Jackson 2.xJackson 3.x
JsonSerializer, JsonDeserializerValueSerializer, ValueDeserializer
SerializerProviderSerializationContext
ModuleJacksonModule
TextNode, JsonNode.asText()StringNode, JsonNode.asString()
JsonNode.fields()JsonNode.properties()

A custom serializer extends ValueSerializer in Jackson 3. Its serialize() method gets a SerializationContext and no longer declares IOException.

public class GramsSerializer3 extends ValueSerializer<Grams> {
  @Override
  public void serialize(Grams grams, JsonGenerator gen, SerializationContext ctxt)
      throws JacksonException {
    gen.writeString(grams.value() + " g");                // {"flour":"250 g"}
  }
}

8. Jackson 3 in Spring Boot 4

Spring Boot 4 creates a Jackson 3 JsonMapper bean for us when spring-boot-starter-jackson is on the classpath, and every web starter brings in that starter. Spring Framework 7 uses the same JsonMapper bean to convert request and response bodies, so our controllers read and write JSON with the Jackson 3 defaults. For example, a GET /ingredients/flour request returns sorted properties and an ISO date.

{"addedOn":"2026-10-05","grams":250,"name":"flour"}

Spring Boot 4 renames its own Jackson classes, and the JSON-specific properties get an extra json part in their names.

Spring Boot 3.xSpring Boot 4.x
Jackson2ObjectMapperBuilderCustomizerJsonMapperBuilderCustomizer
@JsonComponent, @JsonMixin@JacksonComponent, @JacksonMixin
spring.jackson.read.*, spring.jackson.write.*spring.jackson.json.read.*, spring.jackson.json.write.*
ObjectMapper beanJsonMapper bean

To change the mapper that Spring Boot creates, we declare a JsonMapperBuilderCustomizer bean, which keeps Spring Boot’s other settings. The following customizer makes unknown properties fail again, so a POST request with an extra field gets a 400 response.

@Bean
JsonMapperBuilderCustomizer strictReading() {
  return builder -> builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}

8.1. Keeping the Jackson 2 Output During the Migration

Spring Boot 4 has two ways to keep the old JSON. The first keeps Jackson 3 with the Jackson 2 defaults. The second runs Jackson 2 itself through the spring-boot-jackson2 module, which is deprecated and will be removed in a later Spring Boot 4.x release.

# Option 1: Jackson 3 with defaults aligned to Jackson 2
spring.jackson.use-jackson2-defaults=true

# Option 2: Jackson 2 for Spring MVC (needs the spring-boot-jackson2 dependency)
spring.http.converters.preferred-json-mapper=jackson2

With either option, the same request returns {“name”:”flour”,”grams”:250,”addedOn”:”2026-10-05″}. The properties keep field order, and the date stays text because Spring Boot turns off date timestamps in both cases.

9. Migrating With the OpenRewrite Recipe

OpenRewrite is a tool that edits source code with ready-made rules called recipes. Its recipe org.openrewrite.java.jackson.UpgradeJackson_2_3 changes the dependencies, imports and class names. It also removes the built-in modules and the settings that match the new defaults.

mvn -U org.openrewrite.maven:rewrite-maven-plugin:6.46.1:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-jackson:1.29.0 \
  -Drewrite.activeRecipes=org.openrewrite.java.jackson.UpgradeJackson_2_3

The recipe does not move setter calls into the builder. On our Jackson 2 class, it renamed setSerializationInclusion() to setDefaultPropertyInclusion() but left the call on the mapper, which does not compile in Jackson 3. We move these setter calls into JsonMapper.builder() by hand, as in section 3.

10. Jackson 3 Migration Checklist

We fix compile errors first and check the JSON last, because tests run only when the project compiles.

  1. Upgrade to Java 17 or later, and to the latest Jackson 2.x release. Its deprecation warnings show Jackson 3 names.
  2. Replace the com.fasterxml.jackson coordinates with tools.jackson ones, import the jackson-bom, and remove the three built-in modules.
  3. Run the OpenRewrite recipe, or change the imports to tools.jackson by hand, keeping com.fasterxml.jackson.annotation.
  4. Move every ObjectMapper setter into JsonMapper.builder(), and use rebuild() where the code called copy().
  5. Rename the exceptions and the types from section 7.
  6. In a Spring Boot application, move to Spring Boot 4 and rename the classes and properties from section 8.
  7. Run the tests and compare the JSON against the changed defaults. Where needed, turn single features back on or off in the builder.

11. Conclusion

A Jackson 3 migration is mostly renames. We change the coordinates and imports to tools.jackson, keep com.fasterxml.jackson.annotation, move the mapper configuration into JsonMapper.builder() and catch the unchecked JacksonException.

The new defaults need the most attention, because they change the JSON our API reads and writes. In Spring Boot 4, spring.jackson.use-jackson2-defaults keeps the old output while we update the tests one by one.

12. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.