Skip to content
Spring Boot sb web 4 min read

JSON & Jackson

Jackson is the JSON library Spring Boot uses out of the box. The spring-boot-starter-web (and WebFlux) starter pulls it in, and Spring auto-configures an ObjectMapper plus a MappingJackson2HttpMessageConverter so that @RequestBody deserializes JSON and @ResponseBody serializes it automatically. This page shows how to control that serialization with properties, annotations, and custom (de)serializers.

How Spring Boot wires Jackson

When jackson-databind is on the classpath, Spring Boot creates a single shared ObjectMapper bean and uses it for every HTTP message conversion. You rarely build one yourself — you customize the auto-configured instance. The flow for a controller:

public record OrderDto(Long id, String customer, BigDecimal total) {}

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @PostMapping
    public OrderDto create(@RequestBody OrderDto body) {   // JSON → record
        return body;                                       // record → JSON
    }
}

See Request & Response Body for the converter mechanics. Everything below tunes how that JSON looks.

Customizing with properties

The simplest global tuning lives in application.properties under spring.jackson.* — no code required:

# Naming
spring.jackson.property-naming-strategy=SNAKE_CASE
# Drop nulls from output globally
spring.jackson.default-property-inclusion=non_null
# Dates as ISO-8601 strings, not numeric timestamps
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.time-zone=UTC
# Be lenient about unknown incoming fields (default behaviour)
spring.jackson.deserialization.fail-on-unknown-properties=false
PropertyEffect
property-naming-strategySNAKE_CASE, KEBAB_CASE, etc.
default-property-inclusionnon_null, non_empty, non_default
serialization.* / deserialization.*Toggle any Jackson SerializationFeature / DeserializationFeature
time-zone / date-formatDate handling

Customizing the ObjectMapper in code

When properties are not enough, register a Jackson2ObjectMapperBuilderCustomizer bean. It modifies the auto-configured builder without replacing Spring Boot’s sensible defaults:

@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        return builder -> {
            builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
            builder.serializationInclusion(JsonInclude.Include.NON_NULL);
            builder.failOnUnknownProperties(false);
            builder.simpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
        };
    }
}

Warning: Defining your own ObjectMapper @Bean replaces the auto-configured one entirely, and you lose Spring Boot’s registered modules and feature defaults. Prefer the customizer unless you truly need full control.

Field-level annotations

Annotate DTO fields (or record components) to override behavior per property.

public record UserResponse(
        Long id,

        @JsonProperty("full_name")                 // rename in JSON
        String name,

        @JsonIgnore                                 // never serialize
        String passwordHash,

        @JsonInclude(JsonInclude.Include.NON_NULL)  // omit when null
        String nickname,

        @JsonFormat(shape = JsonFormat.Shape.STRING,
                    pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime createdAt) {}

Output (with nickname null):

{
  "id": 42,
  "full_name": "Grace Hopper",
  "createdAt": "2026-06-13 09:30:00"
}
AnnotationPurpose
@JsonProperty("x")Rename a field in JSON
@JsonIgnoreExclude a field both ways
@JsonInclude(NON_NULL)Skip nulls (field or type level)
@JsonFormat(pattern=…)Control date/number formatting
@JsonCreatorMark a constructor/factory for deserialization

@JsonView — different shapes per endpoint

@JsonView lets one model expose different fields depending on the view. Declare view marker classes, tag fields, and select a view on the handler.

public class Views {
    public static class Public {}
    public static class Internal extends Public {}
}

public class Account {
    @JsonView(Views.Public.class)   public Long id;
    @JsonView(Views.Public.class)   public String name;
    @JsonView(Views.Internal.class) public String taxId;   // internal only
}

@GetMapping("/public/{id}")
@JsonView(Views.Public.class)        // taxId is omitted
public Account publicView(@PathVariable Long id) { ... }

@JsonCreator for immutable types

For non-record classes with no default constructor, point Jackson at the constructor to use:

public class Money {
    private final BigDecimal amount;
    private final String currency;

    @JsonCreator
    public Money(@JsonProperty("amount") BigDecimal amount,
                 @JsonProperty("currency") String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    // getters...
}

Tip: Java records are deserialized through their canonical constructor automatically — no @JsonCreator needed. See Records as DTOs.

Handling LocalDateTime — JavaTimeModule

java.time types (LocalDate, LocalDateTime, Instant) need the jackson-datatype-jsr310 module. Spring Boot registers a JavaTimeModule automatically, so this works without setup — but keep write-dates-as-timestamps=false to get readable ISO strings:

public record EventDto(String name, LocalDateTime startsAt) {}

Output:

{ "name": "Launch", "startsAt": "2026-06-13T09:30:00" }

With timestamps enabled (the raw default before Boot’s override), the same field would serialize as a number array — almost never what an API wants.

Custom serializer / deserializer

For special formats, write a JsonSerializer / JsonDeserializer and attach it with @JsonSerialize / @JsonDeserialize:

public class MoneySerializer extends JsonSerializer<BigDecimal> {
    @Override
    public void serialize(BigDecimal value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        gen.writeString("$" + value.setScale(2, RoundingMode.HALF_UP));
    }
}

public record InvoiceDto(
        String id,
        @JsonSerialize(using = MoneySerializer.class) BigDecimal total) {}

Output:

{ "id": "INV-9", "total": "$49.90" }

To apply a custom (de)serializer globally for a type, register it on a SimpleModule inside the Jackson2ObjectMapperBuilderCustomizer via builder.serializerByType(BigDecimal.class, new MoneySerializer()).

Pitfalls

  • A custom ObjectMapper @Bean overrides auto-config — use the customizer instead.
  • @JsonIgnore on a record component works, but field exclusion via Jackson does not stop the data being loaded — strip sensitive fields at the DTO boundary.
  • Snake_case applied globally affects every payload; confirm clients expect it before flipping the switch.
Last updated June 13, 2026
Was this helpful?