Mapping JSON to Java with Jackson: POJOs, records and Lombok
Most Java services spend a lot of time converting JSON to objects and back, and Jackson is the library that does it in Spring Boot, Quarkus, Micronaut and countless other projects. Jackson works out of the box for simple cases, but the shape of your Java classes decides how robust the mapping is when an API adds a field, sends a null or uses a key that is not a valid Java name. This guide explains how Jackson binds JSON, compares POJOs, records and Lombok, and covers the settings that prevent common failures.
How Jackson maps JSON to a class
Given a JSON object, Jackson's ObjectMapper creates an instance of your class and fills in each property. It discovers properties from:
- public fields,
- getters and setters following JavaBean naming (
getEmail,setEmail), - constructor parameters, for records and for constructors annotated with
@JsonCreator.
By default, the JSON key must match the Java property name exactly. Nested JSON objects map to fields whose type is another class, arrays map to List or arrays, and JSON numbers map to whatever numeric type the field declares.
ObjectMapper mapper = new ObjectMapper();
Order order = mapper.readValue(json, Order.class);
String back = mapper.writeValueAsString(order);
Create one ObjectMapper and reuse it; it is thread-safe once configured and expensive to create.
Option 1: Plain POJOs
public class Customer {
private Long id;
private String email;
private List<Address> addresses;
public Customer() {}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... getters and setters for every field
}
POJOs work with every framework and every Jackson version, and are mutable, which some libraries such as JPA require. The downside is boilerplate: equals, hashCode and toString must be written or generated, and every new field adds three or more members that can drift out of sync.
Option 2: Lombok
@Data
@NoArgsConstructor
public class Customer {
private Long id;
private String email;
private List<Address> addresses;
}
Lombok generates getters, setters, equals, hashCode and toString at compile time. The source stays short and Jackson sees ordinary JavaBean methods. For immutable classes, combine @Value with @Builder and @Jacksonized, which tells Jackson to deserialise through the builder. The costs are an annotation processor in the build and IDE support that occasionally lags behind new Java versions.
Option 3: Records
public record Customer(Long id, String email, List<Address> addresses) {}
Records, standard since Java 16, are immutable data carriers with the constructor, accessors, equals, hashCode and toString built in. Jackson 2.12 and later deserialises them through the canonical constructor without any annotations. Records are the best default for DTOs in modern Java: concise, immutable and with no extra dependency. They are not suitable where a framework needs a no-argument constructor and setters, such as JPA entities.
Handling keys that are not valid Java names
JSON keys like first-name, 2fa_enabled or class cannot be Java identifiers. Map them with @JsonProperty:
public record User(
@JsonProperty("first-name") String firstName,
@JsonProperty("2fa_enabled") Boolean twoFactorEnabled) {}
If an API consistently uses snake_case, configure a naming strategy once instead of annotating every field:
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
Unknown properties
By default, Jackson throws UnrecognizedPropertyException when the JSON contains a field your class does not have. That means a third-party API adding a harmless new field can break your service. For data you consume from others, disable this:
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Or annotate individual classes with @JsonIgnoreProperties(ignoreUnknown = true). Spring Boot's auto-configured mapper already disables the feature. Keep it enabled only where strictness is a deliberate choice, such as validating requests to your own API.
Nulls, missing fields and primitives
A JSON field can be present with a value, present with null, or absent. Choose Java types that can represent what you need:
- Wrapper types like
IntegerandBooleanbecomenullwhen the field is missing or null, so you can tell "not sent" from zero or false. - Primitive types like
intandbooleansilently default to 0 and false, hiding missing data. Use them only for fields that are always present. Optionalrequires thejackson-datatype-jdk8module and is generally discouraged for fields; prefer nullable wrappers and validation.
To leave nulls out of serialised output, use @JsonInclude(JsonInclude.Include.NON_NULL) on a class or configure it globally.
Choosing numeric and date types
- Use
Longfor IDs. Values above about 2.1 billion overflowInteger. - Use
BigDecimalfor money.Doublecannot represent many decimal amounts exactly, so totals drift. - Use
java.timetypes such asInstant,OffsetDateTimeandLocalDatefor dates. Register theJavaTimeModuleand disableWRITE_DATES_AS_TIMESTAMPSto read and write ISO 8601 strings.
Generating classes from a sample
Writing classes for a large, nested response by hand is slow and error-prone. Generating them from a real JSON sample gives you correct field names, nested types and list element types immediately. The JSON to Java generator produces POJOs, Lombok classes or records, adds @JsonProperty where keys need it, and merges all objects in an array so optional fields are not missed. Clean up the sample first with the JSON Formatter.
Then review the output with the advice above: change IDs to Long and money to BigDecimal, decide which fields can be null, rename classes to fit your domain, and delete fields you do not need. A sample shows one possible response, not the full contract, so if the API publishes an OpenAPI spec, check the generated types against it.
Recommended defaults
- Use records for DTOs on Java 16 and later.
- Ignore unknown properties for external APIs.
- Use wrapper types,
Longfor IDs,BigDecimalfor money andjava.timefor dates. - Configure naming strategy and modules once, on a shared
ObjectMapper. - Write a test that deserialises a real, saved response, so contract changes are caught in CI.