When writing REST APIs in Spring Boot, you’ve surely run into situations like “LocalDateTime is being returned as an array,” “I want to drop null fields,” or “the frontend wants snake_case.” This article covers which layer to configure Jackson at, the go-to annotations, and how to customize the ObjectMapper. It assumes Spring Boot 3.x / Java 17.

Three Layers of Jackson Configuration

When you add spring-boot-starter-web to a Spring Boot project, Jackson is auto-configured and an ObjectMapper is registered as a Bean. There are roughly three places where you can configure it.

  • spring.jackson.* properties in application.yml (application-wide)
  • Annotations (per DTO class or per field)
  • Bean definitions such as Jackson2ObjectMapperBuilderCustomizer (extending programmatically)

The basic strategy is to put application-wide behavior in YAML, and use annotations when you only want to change a specific field.

Configuring Everything in application.yml

Let’s start with the YAML settings you’ll use most often. This alone covers about 80% of real-world cases.

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Tokyo
    property-naming-strategy: SNAKE_CASE
    default-property-inclusion: non_null
    serialization:
      write-dates-as-timestamps: false
    deserialization:
      fail-on-unknown-properties: false

Setting write-dates-as-timestamps: false makes LocalDateTime output as an ISO-8601 string instead of a numeric array like [2026,5,24,...]. This is the one many people get tripped up by initially.

Formatting Dates and Times

Java 8 time types are handled by the JavaTimeModule from jackson-datatype-jsr310. In Spring Boot 3.x it’s auto-registered, so it works out of the box without adding any dependency.

If you want to change the pattern per field, use @JsonFormat.

public record OrderResponse(
    Long id,
    @JsonFormat(pattern = "yyyy-MM-dd")
    LocalDate orderDate,
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "Asia/Tokyo")
    ZonedDateTime createdAt
) {}

When using ZonedDateTime, specifying the timezone attribute keeps the output offset stable.

Excluding null Fields

It’s very common to not want null in your responses. Pick the right granularity.

@JsonInclude(JsonInclude.Include.NON_NULL)
public record UserResponse(
    Long id,
    String name,
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    List<String> roles,
    String email
) {}
  • NON_NULL excludes only null
  • NON_EMPTY also excludes empty strings, empty collections, and empty Optionals
  • NON_DEFAULT also excludes the default values of primitives (such as 0 or false), so handle it with care

If you want to apply this globally, default-property-inclusion: non_null from earlier is enough.

When You Want snake_case

It’s common for the frontend to expect snake_case like created_at. Globally, property-naming-strategy: SNAKE_CASE is a one-liner; if you want to use a different name only for a specific field, override it with @JsonProperty.

public record ArticleResponse(
    Long id,
    String title,
    @JsonProperty("author")
    String authorName,
    LocalDateTime publishedAt
) {}

The same rule applies to deserialization, so you can also receive requests in snake_case.

Extending with Jackson2ObjectMapperBuilderCustomizer

For settings that can’t be expressed in YAML, or for registering custom modules, use Jackson2ObjectMapperBuilderCustomizer. You could also replace the entire ObjectMapper with a Bean definition, but that undoes Spring Boot’s default behavior, so it’s not recommended.

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> builder
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .modulesToInstall(new MoneyModule());
    }
}

With this approach, you only add diffs to the ObjectMapper that Spring Boot has assembled.

Custom Serializer / Deserializer

For conversions that can’t be expressed with the defaults — currency formatting, custom Enums, and so on — implement JsonSerializer<T> / JsonDeserializer<T>. As an example, here’s a serializer that outputs BigDecimal in ¥1,000 format.

public class YenSerializer extends JsonSerializer<BigDecimal> {
    private static final NumberFormat FORMAT =
        NumberFormat.getCurrencyInstance(Locale.JAPAN);

    @Override
    public void serialize(BigDecimal value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        gen.writeString(FORMAT.format(value));
    }
}

To use it per field, add @JsonSerialize(using = YenSerializer.class). To apply it across the whole application, register it on a SimpleModule and pass that through the modulesToInstall call shown earlier.

When you want to reverse-map an Enum from a display label, implement JsonDeserializer<T>.

public class StatusDeserializer extends JsonDeserializer<Status> {
    @Override
    public Status deserialize(JsonParser p, DeserializationContext ctx)
            throws IOException {
        String label = p.getText();
        return Status.fromLabel(label);
    }
}

Fields You Don’t Want to Output, and Read-Only Fields

Fields you don’t want in the response — like passwords or internal IDs — can be excluded with @JsonIgnore. And when you want to receive a field as input but not output it, @JsonProperty(access = ...) is handy.

public record SignupRequest(
    String email,
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    String password
) {}

WRITE_ONLY removes the field from the response output, and READ_ONLY ignores it on incoming requests. This prevents accidents where confidential information sneaks into the response.

How to Handle Unknown Properties

When a request JSON contains a field you don’t know about, an UnrecognizedPropertyException is thrown by default. For APIs where you want to be tolerant of client-side extensions, it’s common to disable this.

Either set deserialization.fail-on-unknown-properties: false in YAML, or add @JsonIgnoreProperties(ignoreUnknown = true) per class. Conversely, leaving it enabled on internal APIs or endpoints where you want strict consistency lets you catch contract violations early.

If you want to standardize the format of error responses, see also Standardizing error responses with Spring Boot’s Problem Details (RFC 9457). For a basic CRUD implementation, see Tutorial: building a REST API with Spring Boot, and for DTO design, see Writing DTO mapping with MapStruct.

Summary

When shaping JSON, the royal road is to set the broad direction in YAML and then add pinpoint adjustments to your DTOs with annotations. If that’s not enough, add modules through Jackson2ObjectMapperBuilderCustomizer or write a custom Serializer — there’s almost never a need to replace the ObjectMapper as a Bean from the start. When a setting isn’t taking effect, walking through “which layer is overriding this?” in order will usually lead you to the cause.