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
| Property | Effect |
|---|---|
property-naming-strategy | SNAKE_CASE, KEBAB_CASE, etc. |
default-property-inclusion | non_null, non_empty, non_default |
serialization.* / deserialization.* | Toggle any Jackson SerializationFeature / DeserializationFeature |
time-zone / date-format | Date 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@Beanreplaces 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"
}
| Annotation | Purpose |
|---|---|
@JsonProperty("x") | Rename a field in JSON |
@JsonIgnore | Exclude a field both ways |
@JsonInclude(NON_NULL) | Skip nulls (field or type level) |
@JsonFormat(pattern=…) | Control date/number formatting |
@JsonCreator | Mark 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
@JsonCreatorneeded. 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@Beanoverrides auto-config — use the customizer instead. @JsonIgnoreon 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.