Jackson turns JSON into Java objects by matching property names to constructor parameters, setters or fields. When a mapping goes wrong it is nearly always one of four things: a name that does not match, a null that lands in a primitive, a key the class does not declare, or a type Jackson has no module for. The examples use Jackson 2.22 (com.fasterxml.jackson.databind). Jackson 3 moves the databind classes to tools.jackson.databind, so this guide targets 2.x.
Start with a payload whose keys are snake_case and whose timestamp carries no time zone:
{
"user_id": 42,
"display_name": "Ada",
"created_at": "2024-05-01T10:30:00",
"tags": ["admin"],
"nickname": null,
"plan": "pro"
}
A record maps it directly. @JsonProperty binds a JSON name to a component, and JavaTimeModule (from the jackson-datatype-jsr310 artifact) lets Jackson read LocalDateTime; without it Jackson refuses java.time types.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.LocalDateTime;
import java.util.List;
public class Demo {
@JsonIgnoreProperties(ignoreUnknown = true)
public record User(
@JsonProperty("user_id") long userId,
@JsonProperty("display_name") String displayName,
@JsonProperty("created_at") LocalDateTime createdAt,
List<String> tags,
String nickname) {}
public static void main(String[] args) throws Exception {
String json = """
{"user_id": 42, "display_name": "Ada", "created_at": "2024-05-01T10:30:00",
"tags": ["admin"], "nickname": null, "plan": "pro"}""";
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
System.out.println(mapper.readValue(json, User.class));
}
}
It prints User[userId=42, displayName=Ada, createdAt=2024-05-01T10:30, tags=[admin], nickname=null]. The plan key is not in the record, and @JsonIgnoreProperties(ignoreUnknown = true) makes Jackson skip it.
A per-field @JsonProperty is explicit and survives renaming the Java field, which is why the JSON to Java converter emits it when "Add @JsonProperty" is on. When every key follows one convention, a naming strategy is shorter. Put @JsonNaming on the type, or call mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) once to cover every type.
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Order(
long orderId, // reads and writes "order_id"
String shipTo, // "ship_to"
@JsonProperty("15m") int window, // an explicit name wins
@JsonAlias("postcode") String postalCode) {}
An explicit @JsonProperty wins over the strategy, which is how a key such as 15m, not a legal Java identifier, gets a field. @JsonAlias accepts extra incoming names and has no effect on what is written back.
Jackson reads records natively since 2.12. They are immutable and need no configuration beyond the annotations above, so they suit read-only data transfer objects. Choose Lombok when you target a Java version older than 16, need setters, or want builders.
For a @Data class, Jackson calls a no-argument constructor and then the setters, so @NoArgsConstructor is required. The generator always adds it, along with @AllArgsConstructor:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@JsonProperty("product_name")
private String productName;
private Double price;
}
Be careful with @Builder. Combined with those two constructors it is harmless, but on its own it removes the default constructor and Jackson fails with "no delegate- or property-based Creator". Adding @Jacksonized makes Jackson deserialize through the builder instead.
A JSON null and a missing key both leave an object field as null, so Jackson cannot tell them apart. Primitives behave differently: {"count": null} read into an int quietly yields 0. Switching on FAIL_ON_NULL_FOR_PRIMITIVES turns that into a MismatchedInputException. A record component that is missing from the JSON gets the same treatment: null for objects, 0 or false for primitives.
This is why "Use Primitives" is off by default in the converter. With it on, the converter still boxes a key that is null or missing in any sample element, and every list element type. A sample cannot show nulls that real data may contain, so keep the failure setting above on.
Unknown keys are the other default to know. Jackson throws UnrecognizedPropertyException for any key the type does not declare. That is right for configuration you control and wrong for third-party APIs that add fields without notice. The converter adds neither a global setting nor @JsonIgnoreProperties, so add the tolerance where you want it:
// Reject a null for an int, long or boolean instead of storing 0 or false
mapper.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
// Accept payloads that carry keys the class does not declare
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// On output, leave null properties out
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Slim(String a, String b) {}
Because of type erasure, mapper.readValue(json, List.class) returns a list of LinkedHashMap, not your records. Pass new TypeReference<List<User>>() {} to get real objects. The converter accepts only a JSON object at the top level, so wrap an array in a key to generate the model, then use TypeReference for the real array.
On output, JavaTimeModule writes a LocalDateTime as an array of numbers such as [2024,5,1,10,30]. Disable SerializationFeature.WRITE_DATES_AS_TIMESTAMPS to get ISO-8601 strings. Build one ObjectMapper and reuse it: it is thread-safe once configured and expensive to create.
Paste a sample into the JSON to Java converter to get the classes, then read them as a draft. One sample cannot say which keys are optional, whether an Integer should be a Long, or whether an Object stands for a real type. Check those against the API documentation, then add the settings above.