JSON to Java class or record
Style picks the shape:
- Records (default): immutable, one line per field, read through accessors such as
loan.dueOn(). They need Java 16 or newer, and Jackson 2.12 or newer to deserialize. - Classes: the classic POJO with private fields, getters and setters, for older Java or frameworks that want a no-argument constructor. A
booleangetter starts withis, as inisOverdue().
A library loan
With Loan as the Root name, the sample turns into three records:
{
"loanId": "L-55012",
"book": {
"isbn": "978-0-14-118776-1",
"title": "Middlemarch",
"author": "George Eliot"
},
"member": { "cardNumber": "M-2209", "name": "Rosa Diaz" },
"borrowedOn": "2026-10-20",
"dueOn": "2026-11-10",
"renewals": 1,
"overdue": false
}
import com.fasterxml.jackson.annotation.JsonProperty;
public record Loan(
@JsonProperty("loanId") String loanId,
@JsonProperty("book") Book book,
@JsonProperty("member") Member member,
@JsonProperty("borrowedOn") String borrowedOn,
@JsonProperty("dueOn") String dueOn,
@JsonProperty("renewals") long renewals,
@JsonProperty("overdue") boolean overdue
) {}
public record Member(
@JsonProperty("cardNumber") String cardNumber,
@JsonProperty("name") String name
) {}
public record Book(
@JsonProperty("isbn") String isbn,
@JsonProperty("title") String title,
@JsonProperty("author") String author
) {}
@JsonProperty carries the JSON key on every component, so renaming a Java field later can’t break the mapping. Keys that are Java keywords get a suffix: default becomes defaultValue. borrowedOn and dueOn stay String; for LocalDate, change the type and register Jackson’s Java time module (jackson-datatype-jsr310 in Jackson 2).
Primitive or boxed types
A field that is null or missing anywhere gets a boxed type, as in this array with the root name Holdings:
[
{ "isbn": "978-0-14-118776-1", "copies": 3, "reference": false },
{ "isbn": "978-0-19-953556-5", "copies": null }
]
import com.fasterxml.jackson.annotation.JsonProperty;
public record Holding(
@JsonProperty("isbn") String isbn,
@JsonProperty("copies") Long copies,
@JsonProperty("reference") Boolean reference
) {}
A long can’t hold null, so Jackson 2 fills in 0 and you can’t tell “no value” from zero; Long keeps the difference. List elements are always boxed (List<Long>), since generics don’t take primitives. The merge that finds these fields is described on JSON to TypeScript.
Reading JSON with Jackson
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Loan loan = mapper.readValue(Files.readString(Path.of("loan.json")), Loan.class);
System.out.println(loan.book().title() + " is due " + loan.dueOn());
}
}
For a top-level array, pass a type reference: mapper.readValue(json, new TypeReference<List<Holding>>() {}). Jackson 2 throws UnrecognizedPropertyException on undeclared keys, so add @JsonIgnoreProperties(ignoreUnknown = true) if the API may add fields.
Before you compile
Java allows one public top-level type per file, but the output holds them all. Put Loan, Book and Member in their own files, or nest Book and Member inside Loan. Values whose type varies, or that are always null, become Object. If the data arrives as XML, convert it with XML to JSON first. For Kotlin, use JSON to Kotlin.