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.
| Change | Jackson 2.x | Jackson 3.x | What we do |
|---|---|---|---|
| Maven group id | com.fasterxml.jackson.core | tools.jackson.core | Change the coordinates (section 2) |
| Java packages | com.fasterxml.jackson.* | tools.jackson.* | Change the imports |
| Annotations | com.fasterxml.jackson.annotation | Unchanged (2.x artifact) | Keep the annotation imports |
| Minimum Java | Java 8 | Java 17 | Upgrade the JDK first |
| ObjectMapper | Changed with setters after creation | Immutable, built once with JsonMapper.builder() | Move setters into the builder (section 3) |
| Base exception | JsonProcessingException (checked) | JacksonException (unchecked) | Update catch blocks (section 4) |
| java.time, Optional, parameter names | Separate modules | Built into jackson-databind | Remove the modules (section 5) |
| Defaults | Dates as timestamps, unknown properties fail | ISO dates, unknown properties ignored, properties sorted | Check the JSON output (section 6) |
| Serializers | JsonSerializer, SerializerProvider | ValueSerializer, SerializationContext | Rename the classes (section 7) |
| Spring Boot | Spring Boot 3.x uses Jackson 2 | Spring Boot 4.x uses Jackson 3 | Rename 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.

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.x | Jackson 3.x |
|---|---|
| JsonMappingException | DatabindException |
| JsonParseException | StreamReadException |
| JsonGenerationException | StreamWriteException |
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) | Input | Jackson 2.x | Jackson 3.x |
|---|---|---|---|
| DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS | LocalDate.of(2026, 10, 5) | [2026,10,5] | “2026-10-05” |
| DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPS | Duration.ofMinutes(25) | 1500.000000000 | “PT25M” |
| MapperFeature.SORT_PROPERTIES_ALPHABETICALLY | Class with fields name, grams, vegan | name, grams, vegan | grams, name, vegan |
| EnumFeature.WRITE_ENUMS_USING_TO_STRING | EASY with toString() returning “easy” | “EASY” | “easy” |
| DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES | JSON with an extra rating field | UnrecognizedPropertyException | Field ignored |
| DeserializationFeature.FAIL_ON_TRAILING_TOKENS | Two JSON objects in one string | First object read | MismatchedInputException |
| DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES | {“grams”:null} into an int | grams = 0 | MismatchedInputException |
| SerializationFeature.FAIL_ON_EMPTY_BEANS | Class without properties | InvalidDefinitionException | {} |
| MapperFeature.DEFAULT_VIEW_INCLUSION | @JsonView active, author field without a view | stars and author | stars 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.x | Jackson 3.x |
|---|---|
| JsonSerializer, JsonDeserializer | ValueSerializer, ValueDeserializer |
| SerializerProvider | SerializationContext |
| Module | JacksonModule |
| 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.x | Spring Boot 4.x |
|---|---|
| Jackson2ObjectMapperBuilderCustomizer | JsonMapperBuilderCustomizer |
| @JsonComponent, @JsonMixin | @JacksonComponent, @JacksonMixin |
| spring.jackson.read.*, spring.jackson.write.* | spring.jackson.json.read.*, spring.jackson.json.write.* |
| ObjectMapper bean | JsonMapper 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.
- Upgrade to Java 17 or later, and to the latest Jackson 2.x release. Its deprecation warnings show Jackson 3 names.
- Replace the com.fasterxml.jackson coordinates with tools.jackson ones, import the jackson-bom, and remove the three built-in modules.
- Run the OpenRewrite recipe, or change the imports to tools.jackson by hand, keeping com.fasterxml.jackson.annotation.
- Move every ObjectMapper setter into JsonMapper.builder(), and use rebuild() where the code called copy().
- Rename the exceptions and the types from section 7.
- In a Spring Boot application, move to Spring Boot 4 and rename the classes and properties from section 8.
- 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
- Jackson 3 migration guide
- Jackson 3.0 release notes
- JSTEP-2, default setting changes
- JSTEP-6, renamed classes and methods
- Introducing Jackson 3 support in Spring
- Spring Boot 4.0 migration guide
- Spring Boot JSON reference
- OpenRewrite recipe UpgradeJackson_2_3
Happy Learning !!