Jackson 3 in Spring Boot 4

Spring Boot 4 moves to Jackson 3: new tools.jackson packages, renamed types, immutable mappers, unchecked exceptions and some changed defaults.

Syntaximport tools.jackson.databind.json.JsonMapper; // databind: new package import com.fasterxml.jackson.annotation.JsonProperty; // annotations: unchanged

Spring Boot 4 and Spring Framework 7 are built on Jackson 3. Most of your DTOs keep working untouched, because the annotations you use every day (@JsonProperty, @JsonInclude, @JsonFormat, @JsonIgnore) still live in com.fasterxml.jackson.annotation. What changes is everything else: packages, class names, how a mapper is configured and a handful of defaults.

New packages and names

Core and databind moved from com.fasterxml.jackson to tools.jackson. Several types were renamed at the same time:

Spring Boot 3 / Jackson 2Spring Boot 4 / Jackson 3
JsonSerializer, JsonDeserializerValueSerializer, ValueDeserializer
SerializerProviderSerializationContext
ModuleJacksonModule
@JsonComponent@JacksonComponent
Jackson2ObjectMapperBuilderCustomizerJsonMapperBuilderCustomizer
MappingJackson2HttpMessageConverterJacksonJsonHttpMessageConverter
gen.writeStringField(...)gen.writeStringProperty(...)

Watch the databind annotations: @JsonSerialize, @JsonDeserialize and @JsonNaming are now in tools.jackson.databind.annotation, while the core annotations did not move. Search your code base for com.fasterxml.jackson.databind; anything left is a compile error waiting to happen.

Immutable mappers and unchecked exceptions

A Jackson 3 ObjectMapper cannot be reconfigured after it is built: there is no configure() or registerModule(). All settings go through JsonMapper.builder(), which is why Spring Boot exposes builder customisers. The upside is that a shared mapper can no longer be changed by one careless call at runtime.

JacksonException now extends RuntimeException. Code that wrapped every writeValueAsString in try/catch (JsonProcessingException e) can drop the boilerplate, but catch JacksonException deliberately where bad input is expected, such as webhook payloads.

Defaults that changed

Support for java.time is built in, so the separate JSR-310 module is gone. Dates are written as ISO strings by default. Properties of ordinary classes are sorted alphabetically, and a null for a primitive is rejected. If an upgrade changes your responses and you need time to adapt clients, spring.jackson.use-jackson2-defaults=true configures Jackson 3 with the defaults Spring Boot used for Jackson 2. Treat it as a bridge, not a destination.

Example

Example · java
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.module.SimpleModule;

record Temperature(double celsius) {}

// Jackson 2: extends JsonSerializer<T> and takes a SerializerProvider.
class TemperatureSerializer extends ValueSerializer<Temperature> {

    @Override
    public void serialize(Temperature value, JsonGenerator gen, SerializationContext context) {
        gen.writeStartObject();
        gen.writeNumberProperty("celsius", value.celsius());
        gen.writeStringProperty("unit", "C");
        gen.writeEndObject();
    }
}

@Configuration(proxyBeanMethods = false)
public class JsonConfig {

    // Jackson 2: Jackson2ObjectMapperBuilderCustomizer.
    @Bean
    JsonMapperBuilderCustomizer temperatureCustomizer() {
        return builder -> builder.addModule(
                new SimpleModule("temperature").addSerializer(Temperature.class, new TemperatureSerializer()));
    }
}

When to use it

  • Upgrade a Spring Boot 3 service whose custom serialisers extend JsonSerializer and fail to compile on Boot 4.
  • Keep a partner integration's byte-for-byte JSON stable for one release while the upgrade lands.
  • Remove the try/catch boilerplate around writeValueAsString that Jackson 2's checked exceptions forced on every caller.

More examples

Import changes at a glance

Work through this list with your IDE's organise-imports rather than a blind text replacement. The last line is the trap: the annotations package did not change, and the Jackson 3 annotations jar still provides it.

Example · text
Jackson 2 (Spring Boot 3)                                Jackson 3 (Spring Boot 4)
com.fasterxml.jackson.databind.ObjectMapper              tools.jackson.databind.ObjectMapper
com.fasterxml.jackson.databind.json.JsonMapper           tools.jackson.databind.json.JsonMapper
com.fasterxml.jackson.databind.JsonSerializer            tools.jackson.databind.ValueSerializer
com.fasterxml.jackson.databind.JsonDeserializer          tools.jackson.databind.ValueDeserializer
com.fasterxml.jackson.databind.SerializerProvider        tools.jackson.databind.SerializationContext
com.fasterxml.jackson.databind.annotation.JsonNaming     tools.jackson.databind.annotation.JsonNaming
com.fasterxml.jackson.core.JsonProcessingException       tools.jackson.core.JacksonException (unchecked)
org.springframework.boot.jackson.JsonComponent           org.springframework.boot.jackson.JacksonComponent
com.fasterxml.jackson.annotation.JsonProperty            com.fasterxml.jackson.annotation.JsonProperty (same)

Keeping the old defaults during an upgrade

Use this when an upgrade changes response bodies that a mobile app or partner integration compares byte for byte. Plan to remove it: write tests that pin the JSON you actually want, then switch the flag off and fix the differences one by one.

Example · properties
# Configure Jackson 3 with the defaults Spring Boot used with Jackson 2:
# properties of plain classes keep declaration order, null for a primitive
# becomes 0 again, java.util.Date keeps its old offset format.
spring.jackson.use-jackson2-defaults=true

Unchecked exceptions when parsing untrusted input

Because <code>JacksonException</code> is a <code>RuntimeException</code>, the compiler no longer reminds you that parsing can fail. Catch it on purpose at the boundaries where input is untrusted, and let it propagate where a failure really is a bug.

Example · java
import java.util.Optional;
import org.springframework.stereotype.Component;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.json.JsonMapper;

record WebhookEvent(String type, String id) {}

@Component
public class WebhookParser {

    private final JsonMapper jsonMapper;

    WebhookParser(JsonMapper jsonMapper) {
        this.jsonMapper = jsonMapper;
    }

    Optional<WebhookEvent> parse(String payload) {
        try {
            return Optional.of(jsonMapper.readValue(payload, WebhookEvent.class));
        } catch (JacksonException e) {
            // Bad payloads are expected from the outside world; do not let them become a 500.
            return Optional.empty();
        }
    }
}

Discussion

  • Be the first to comment on this lesson.