Content Negotiation & Jackson
Content Negotiation & Jackson
Every time a Spring Boot REST controller returns a Java object, something has to convert it into bytes on the wire. That something is Jackson — the JSON library that Spring Boot auto-configures the moment you add spring-boot-starter-web. Understanding how Jackson serializes and deserializes your objects, and how to bend it to your API's exact needs, is a skill you will use on every project.
What Is Content Negotiation?
Content negotiation is the HTTP mechanism by which a client and server agree on the format of the response. The client expresses preference via the Accept request header (e.g. Accept: application/json or Accept: application/xml). The server picks the best matching format it can produce and declares it in the Content-Type response header.
Spring MVC's ContentNegotiationManager sits in front of every @RestController method. It inspects the Accept header, enumerates the HttpMessageConverter beans registered in the application context, and selects the converter that can both handle the return type and produce the requested media type. Jackson's converter, MappingJackson2HttpMessageConverter, handles application/json (and application/*+json). If no converter matches, Spring returns 406 Not Acceptable.
spring-boot-starter-web on the classpath, JSON is the only auto-configured format. Adding jackson-dataformat-xml (or spring-boot-starter-web + jackson-dataformat-xml) transparently enables XML negotiation alongside JSON — zero extra configuration.
How Jackson Serializes a Java Object
Jackson's ObjectMapper walks a Java object by reflection (or, in newer versions, via bytecode generation) and emits JSON. By default it includes every public getter as a JSON field, using the property name derived from the getter method name. A field getUserName() becomes "userName" in the output.
Consider this entity:
A controller returning an Employee produces, by default:
Notice hireDate renders as an array. That is Jackson's default for java.time.LocalDate — almost never what you want. The fix is covered below.
Jackson Annotations You Will Use Daily
@JsonProperty — override the JSON key for a single field:
@JsonIgnore — exclude a field from both serialization and deserialization:
@JsonInclude — suppress null fields (or empty collections) from the output, keeping responses lean:
@JsonFormat — control how dates and numbers are serialized:
Now hireDate renders as "2023-03-15" instead of the array. This is the most common fix for date issues.
@JsonNaming — apply a naming strategy to every property of a class at once, without annotating each field individually:
Configuring Jackson Globally via application.properties
Most Jackson settings can be toggled through Spring Boot's spring.jackson.* property namespace, which avoids writing any Java config:
application.properties for global settings. Annotation-level overrides (@JsonFormat, @JsonProperty) are for exceptions to that global policy. This keeps your domain classes clean and centralizes the serialization strategy where it is easy to audit and change.
Registering a Custom ObjectMapper Bean
When you need programmatic control — registering a custom serializer, enabling a feature that has no property equivalent, or configuring a JavaTimeModule manually — declare an ObjectMapper (or Jackson2ObjectMapperBuilder) bean:
ObjectMapper bean in the context. Your bean fully replaces the default one, so make sure to re-apply any settings you relied on implicitly (e.g. registering JavaTimeModule for java.time support).
Writing a Custom Serializer
Sometimes you need serialization logic that no annotation can express — for example, formatting a BigDecimal as a currency string, or masking part of a sensitive field. Extend JsonSerializer<T>:
Register it on the field:
Controlling Deserialization: Handling Unknown Fields
By default Jackson throws UnrecognizedPropertyException when an incoming JSON body contains a field your DTO does not declare. This is safe but brittle — it breaks your API every time a client sends a newer payload. The production-ready approach is to ignore unknown fields globally:
Or at class level for specific DTOs:
CreateEmployeeRequest and EmployeeResponse) and map between them. This protects against mass-assignment vulnerabilities and lets your schema evolve independently of your API surface.
Summary
Spring Boot wires Jackson as the default JSON engine for all REST controllers. You control its output at three levels: globally via spring.jackson.* properties, per-class via annotations like @JsonNaming and @JsonInclude, and per-field via @JsonProperty, @JsonIgnore, @JsonFormat, and custom serializers. Always use dedicated DTO classes rather than exposing JPA entities, configure write-dates-as-timestamps=false to get readable ISO dates, and disable fail-on-unknown-properties for a more resilient API contract. In the next lesson you will apply these skills to API versioning and REST best practices.