Configuring the JSON Mapper

Tune the auto-configured JsonMapper with spring.jackson properties, builder customisers and module beans, without accidentally replacing it.

Syntaxspring.jackson.deserialization.fail-on-unknown-properties=true @Bean JsonMapperBuilderCustomizer c() { return builder -> builder.enable(...); }

Spring Boot builds one JsonMapper for the application. You can change it in three ways, from least to most code, and you should use the first one that does the job.

1. Properties

Jackson's feature flags can be switched under spring.jackson.*, in groups such as serialization, deserialization, mapper, json.read, json.write, datatype.datetime and datatype.enum, plus top-level settings such as property-naming-strategy, default-property-inclusion and time-zone. Feature names are the enum constants in kebab-case, so DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES becomes spring.jackson.deserialization.fail-on-unknown-properties. Properties can differ per profile, which is useful for pretty-printed output in development only.

2. Customisers and modules

A JsonMapperBuilderCustomizer bean receives the JsonMapper.Builder after Boot has applied the properties, and can call anything on it. Customisers add to Boot's configuration rather than replacing it, and you can have as many as you like. Any JacksonModule bean, such as a SimpleModule with your serialisers, is registered with the mapper automatically.

3. Replacing the mapper

Defining your own JsonMapper bean makes Boot's auto-configured one back off. If you build it with JsonMapper.builder(), every spring.jackson.* property and every customiser silently stops working, and so does the Problem Details support Boot adds to the mapper. This is the classic mistake: someone adds a second mapper bean for one external API and the whole application's JSON changes.

When one integration needs different rules, inject the JsonMapper.Builder bean instead. It is prototype-scoped and already carries Boot's settings, so each injection point gets a fresh builder that it can adjust and build privately without affecting anything else.

Strict or lenient input

Unknown properties are ignored by default. That is friendly for evolving clients but hides typos: a client sending "quanity" gets a 200 and a missing value. Internal APIs often benefit from fail-on-unknown-properties=true; public APIs usually stay lenient so older clients keep working when you remove a field.

Jackson also enforces size limits on incoming documents. The Jackson 3 defaults are generous: strings of up to 100 million characters, 500 levels of nesting and numbers of up to 1000 digits. When your payloads are small and predictable, tighten them under spring.jackson.factory.constraints.read.* so an oversized or deeply nested document is rejected early, and keep a request size limit at the server or gateway as well.

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.DeserializationFeature;
import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.std.StdSerializer;

record Sku(String value) {}

class SkuSerializer extends StdSerializer<Sku> {
    SkuSerializer() {
        super(Sku.class);
    }

    @Override
    public void serialize(Sku sku, JsonGenerator gen, SerializationContext context) {
        gen.writeString(sku.value());
    }
}

@Configuration(proxyBeanMethods = false)
public class JacksonConfig {

    // Adds to Boot's configuration; spring.jackson.* properties still apply.
    @Bean
    JsonMapperBuilderCustomizer strictInput() {
        return builder -> builder
                .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
    }

    // Any JacksonModule bean is registered with the auto-configured mapper.
    @Bean
    SimpleModule skuModule() {
        return new SimpleModule("sku").addSerializer(Sku.class, new SkuSerializer());
    }
}

When to use it

  • Reject request bodies with misspelt property names on an internal API so client bugs show up in testing.
  • Pretty-print JSON in the dev profile only, without touching production output.
  • Talk to a partner that requires snake_case without changing the naming of your own API.

More examples

The same settings as properties

Everything the customiser above does with feature flags can be done here, and property values can change per profile without a code change. The read constraints here are lower than Jackson 3's defaults (100 million characters per string, 500 levels of nesting), which suits an API whose bodies are small DTOs. Leave them at the defaults if you accept large values such as base64 file uploads.

Example · yaml
spring:
  jackson:
    deserialization:
      fail-on-unknown-properties: true
    mapper:
      accept-case-insensitive-enums: true
    serialization:
      indent-output: false
    factory:
      constraints:
        read:
          max-string-length: 1000000
          max-nesting-depth: 50

A private mapper for one partner API

The partner gets <code>tracking_number</code> while every controller keeps camelCase. Because the mapper is not a bean, Boot's primary <code>JsonMapper</code> is untouched; registering it as a <code>@Bean</code> would have replaced the application's mapper.

Example · java
import org.springframework.stereotype.Component;
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.json.JsonMapper;

record PartnerShipment(String trackingNumber, String carrierCode) {}

@Component
public class PartnerPayloads {

    private final JsonMapper partnerMapper;

    // JsonMapper.Builder is a prototype bean: this builder already has Boot's
    // settings and changing it affects nobody else.
    PartnerPayloads(JsonMapper.Builder builder) {
        this.partnerMapper = builder
                .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
                .build();
    }

    String toPartnerJson(PartnerShipment shipment) {
        return partnerMapper.writeValueAsString(shipment);
    }
}

Unit testing a customiser

A customiser is a plain function of the builder, so you can test it without starting Spring. In a running application the same failure reaches Spring MVC as an unreadable body, and the client gets a 400 instead of a silently ignored typo.

Example · java
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.Test;
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.exc.UnrecognizedPropertyException;
import tools.jackson.databind.json.JsonMapper;

class StrictInputCustomizerTest {

    record Item(String name) {}

    final JsonMapperBuilderCustomizer strictInput =
            builder -> builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

    @Test
    void rejectsUnknownProperties() {
        JsonMapper.Builder builder = JsonMapper.builder();
        strictInput.customize(builder);
        JsonMapper mapper = builder.build();

        assertThatThrownBy(() -> mapper.readValue("{\"name\":\"a\",\"nmae\":\"b\"}", Item.class))
                .isInstanceOf(UnrecognizedPropertyException.class);
    }
}

Discussion

  • Be the first to comment on this lesson.