Understanding JSON → Java
Jackson, and a hundred annotations.
The two-decade tradition of Java JSON handling, why records changed everything in Java 14, and the annotations that distinguish a working POJO from a broken one.
Jackson is the de-facto library.
Java has no JSON support in the standard library. Every codegen tool assumes Jackson (com.fasterxml.jackson.databind) — the most-used Java library on Maven Central, the default in Spring Boot, the basis for almost every server-side framework's serialization. The generated POJOs look like normal Java but get annotated with Jackson's @JsonProperty, @JsonIgnoreProperties, @JsonInclude markers that tell the library how to map fields.
Classic POJO vs record.
A classic POJO has private fields, a no-arg constructor, getters, setters, and an equals/hashCode pair — easily 80 lines for a 5-field DTO. Java 14 records collapse the same idea to one line: record User(@JsonProperty("user_id") int userId, String name) {}. Records are immutable, generate equals/hashCode/toString automatically, and read like the data they describe. Modern codegen tools emit records by default and fall back to classic POJOs only when the project targets Java 8 or 11.
The @JsonProperty annotation.
JSON keys are typically snake_case; Java conventions are camelCase. Bridging them is one annotation: @JsonProperty("user_id") on a field named userId tells Jackson to read and write the JSON as user_id regardless of what Java calls it. Without the annotation, Jackson looks for an exact name match — and silently treats the field as missing if it can't find it. The most common Java JSON bug is "the field is always null after deserialization", and 80 % of the time it's a missing or misspelled @JsonProperty.
A worked example.
From { "user_id": 7, "name": "Q", "tags": ["a"] } a record emits: public record User(
@JsonProperty("user_id") int userId,
String name,
List<String> tags
) {} A classic POJO for the same shape is roughly five times longer — class declaration, private fields, a no-arg constructor, a full-args constructor, five getters and setters, plus Jackson's annotations on the fields. Records exist because the boilerplate actually got embarrassing.
Record form
Java 14+
One line; immutable; equals + hashCode + toString automatic.
record User(int userId, String name, List<String> tags) {}
= 3 lines, fully featured
Classic POJO
Java 8 / 11
No-arg ctor + getters + setters + equals/hashCode.
class User { private int userId; public int getUserId() {...} ... }
= ~80 lines for 3 fields
Optional and nullable types.
Java has two notions of "missing": primitive types (int, boolean) can't be null at all and default to their zero value; wrapper types (Integer, Boolean) can be null. Use wrappers when the JSON field is genuinely optional. Optional<T> is also valid, but is conventionally for return types, not fields — Jackson supports both. The choice matters for the same reason it matters in Go: a primitive int defaulting to 0 silently masks "this field was missing".
Polymorphic types — @JsonTypeInfo.
For tagged unions (an array of events of different shapes), Jackson supports @JsonTypeInfo(use = Id.NAME, property = "type") with @JsonSubTypes listing the possible variants. Done correctly, you get a base class or sealed interface and Jackson dispatches the right concrete type from the JSON's discriminator field. Codegen tools recognise discriminator patterns and emit the right shape; without explicit support, you get a flat class with every field optional, which is functionally broken.
Strictness — fail or ignore unknown fields.
By default Jackson fails when deserializing JSON with fields the target class doesn't have. For an internal API that's the right default — it catches drift. For a public- consumer scenario where the producer might add fields, you want @JsonIgnoreProperties(ignoreUnknown = true) on the class (or the global ObjectMapper setting). Choose explicitly per type; the default is good for service-to- service, not for evolving APIs.
Builders and Lombok.
Before records, projects often used Lombok's @Value and @Builder to eliminate boilerplate. They still work fine — and remain the go-to in projects that can't use records yet — but a record on Java 21 plus Jackson 2.15 is comparable in terseness without the annotation-processor dance. New projects on modern Java should prefer records; brownfield projects can keep Lombok.