In the previous chapter, you saw Spring Boot automatically convert Java objects to JSON responses and JSON request bodies back into Java objects. That conversion is handled by Jackson, the JSON library included through the Spring Web dependency we added with Spring Initializr.
In this chapter, you will look more closely at message formats: how Jackson performs those conversions, how to customize JSON field mapping, why DTOs matter, how nested objects work, and how Spring Boot chooses response formats using HTTP headers.
In Spring Boot with Spring Web, JSON is the default format for most REST API request and response bodies. You usually do not configure this yourself. Spring Boot detects Jackson on the classpath and registers the HTTP message converters needed for JSON.
Why JSON? It’s lightweight, human-readable, maps naturally to objects and arrays, and is supported by almost every REST API client and server framework.
In Core Program’s File processing, you have already worked with JSON where you used JSON.parse() and JSON.stringify() after reading JSON as a string. The idea here is similar, but Jackson does the parsing and stringifying for your Spring Boot application.
Object to JSON and backJackson handles two directions of conversion:
<aside> 💡
To memorize serialization vs deserialization, it can help to remember what JSON is. A data-interchange format.
Then from your application’s perspective, objects in deserialized form, need to be packaged for transport and need serialization. Whereas objects in serialized form need to be unpacked after transport: deserialization.
</aside>
In both directions, Jackson matches JSON property names to Java property names. A Java property named image maps to a JSON field named "image". Same spelling, same capitalization.
For the Category class from the previous chapter, Jackson can serialize this object:
Category category = new Category(1L, "Clothes", "<https://i.imgur.com/QkIa5tT.jpeg>");
Into JSON and back:
{
"id": 1,
"name": "Clothes",
"image": "<https://i.imgur.com/QkIa5tT.jpeg>"
}
Jackson automatically serializes and deserializes Java objects/JSON using reflection combined with heuristics*.
<aside> 💡
A heuristic is an estimation of the solution that helps algorithms make decisions quickly, without exhausting all possible options.
They rely on common patterns that work well in practice rather than being enforced like a contract.
</aside>
One of Jackson heuristics decide which fields, getters, setters, or constructors to use in the conversion process.
The exact mechanism are an implementation detail of Jackson. They can (and do) change between versions as the library improves its auto-detection logic. While this makes Jackson very convenient and productive, it can occasionally lead to unexpected behavior.
To avoid relying on these internal heuristics, Jackson provides explicit annotations that let you precisely control how properties are mapped, with minimal code.
Jackson also offers many configuration options and feature flags that can influence its behavior. However, in this course we will focus on the annotations, as they cover the large majority of real-world use cases.
You can always explore the configuration options later when you need more advanced control.
Jackson can work with regular classes, Lombok-generated getters and setters, and Java records. The exact construction path depends on the type:
Category, Jackson commonly uses a no-argument constructor and setters.record, Jackson might use the record's canonical constructor*.<aside> 💡
A canonical constructor* is the constructor Java automatically defines for every record. It accepts all record components as parameters.
For this record:
public record CategoryRequest(String name, String image) {}
the canonical constructor looks like this:
Category(String name, String image) {
this.name = name;
this.image = image;
}
</aside>
As an application developer, the practical rule is: keep your JSON field names and Java property names aligned. When they cannot match, use an annotation such as @JsonProperty , covered below.
Sometimes the default mapping is not what you want. Jackson annotations let you customize how objects are serialized and deserialized. These are runtime annotations processed by Jackson while your application handles requests and responses.
@JsonProperty@JsonProperty changes the JSON field name without renaming your Java field. This is useful when an API uses a naming convention such as snake_case, while Java code uses camelCase.
For example, the API may use image_url:
{
"id": 1,
"name": "Clothes",
"image_url": "<https://i.imgur.com/QkIa5tT.jpeg>"
}
You can map image_url to a Java field named imageUrl :
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonProperty("image_url")
private String imageUrl;
@JsonIgnore@JsonIgnore excludes a property from JSON. With basic field usage, it prevents that property from appearing in responses and ignores it in request bodies.
Use it for sensitive or internal-only data:
import lombok.Getter;
import lombok.Setter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Getter
@Setter
public class User {
private Long id;
private String email;
@JsonIgnore
private String passwordHash;
}
The JSON response will not include passwordHash:
{
"id": 1,
"email": "[email protected]"
}
<aside> 💡
This annotation can be useful for hiding away fields that you don’t think is needed for the client or that can improve performance.
You may also see @JsonIgnore used later to break circular references between database entities.
@Entity
public class User {
@OneToMany(mappedBy = "user")
@JsonIgnore // ← Break the loop here
private List<Post> posts;
}
@Entity
public class Post {
@ManyToOne
private User user;
}
</aside>
@JsonFormat@JsonFormat controls how values are formatted. It is common for dates and times.
By default, a LocalDate is serialized in ISO format:
{
"createdAt": "2026-04-25"
}
You can change that format with @JsonFormat:
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CategoryResponse {
@JsonFormat(pattern = "MM-dd-yyyy")
private LocalDate createdAt;
}
Then the JSON becomes:
{
"createdAt": "04-25-2026"
}
<aside> ⚠️
Custom date formats can make an API harder to use. Prefer standard ISO formats unless you have a clear reason to change them.
</aside>
@JsonInclude@JsonInclude controls which properties are included in JSON output. A common use is leaving out properties whose value is null.
Imagine your category list contains one category without an image:
[
{
"id": 1,
"name": "Clothes",
"image": "<https://i.imgur.com/QkIa5tT.jpeg>"
},
{
"id": 2,
"name": "Shoes",
"image": null
}
]
You can exclude null values like this:
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Category {
private Long id;
private String name;
private String image;
}
Then the second item will serialized without the image field:
[
{
"id": 1,
"name": "Clothes",
"image": "<https://i.imgur.com/QkIa5tT.jpeg>"
},
{
"id": 2,
"name": "Shoes"
}
]
<aside> 💡
</aside>
| --- | --- | --- |
<aside> ⚠️
</aside>
In Writing Endpoints chapter, the Category class served as both the internal model and the request/response body. That was useful for a first controller, but real applications often need different shapes for input and output.
A DTO (Data Transfer Object) is a simple object whose purpose is to carry data between the client and the server. A DTO usually has no business logic. It describes the API contract: what the client may send and what the server may return.
In the previous chapter's createCategory() endpoint, the server assigned the id itself. But because Category has an id field, nothing stops a client from sending this request body:
{
"id": 999,
"name": "Shoes",
"image": "<https://i.imgur.com/1twoaDy.jpeg>"
}
We had to remember to overwrite the client-provided id in the endpoint. If we forgot, the client's value could accidentally become part of our data.