RizTech Academy logo
RizTech Academy
Files, Dates and DataLesson 4 of 430 min

JSON with Jackson

JSON is what APIs speak. Every REST endpoint you call or write, every config file in a modern project, every message on a queue. Java has no JSON support in the standard library, so you need a dependency — and in practice that means Jackson, which is what Spring Boot uses by default and what you will meet in almost every Java job.

This is also the first lesson using a library that is not part of the JDK. Module 10 shows how Maven fetches it; for now, know that the three jars jackson-databind, jackson-core and jackson-annotations are what you need, plus jackson-datatype-jsr310 for dates.

ObjectMapper

One object does nearly everything.

ObjectMapper mapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Create one and reuse it. It is thread-safe after configuration and expensive to build, so a static final field is right. Creating one per request is a common performance mistake.

Object to JSON

record Subscriber(String name, String area, int tiffins, LocalDate startedOn) { }

Subscriber priya = new Subscriber("Priya Deshmukh", "Wagholi", 26, LocalDate.of(2026, 9, 1));
mapper.writeValueAsString(priya);
{"name":"Priya Deshmukh","area":"Wagholi","tiffins":26,"startedOn":"2026-09-01"}
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(priya);
{
  "name" : "Priya Deshmukh",
  "area" : "Wagholi",
  "tiffins" : 26,
  "startedOn" : "2026-09-01"
}

Records work with no annotations at all since Jackson 2.12. The component names become the field names. This is the strongest practical reason to use records for anything crossing an API boundary, and it is why the records lesson listed DTOs first.

JSON to object

String json = """
        {"name":"Arjun Kale","area":"Kharadi","tiffins":18,"startedOn":"2026-09-05"}
        """;
Subscriber arjun = mapper.readValue(json, Subscriber.class);
Subscriber[name=Arjun Kale, area=Kharadi, tiffins=18, startedOn=2026-09-05]
accessor: SATURDAY

The date came back as a real LocalDate, so getDayOfWeek() works immediately.

For a list, the class literal is not enough — erasure again — so Jackson gives you TypeReference:

List<Subscriber> back = mapper.readValue(listJson, new TypeReference<List<Subscriber>>() {});
round trip size: 2, equal: true

That new TypeReference<...>() {} with empty braces is an anonymous subclass, which is how the type argument survives erasure. You will copy this line for the rest of your career.

Dates need the module

new ObjectMapper().writeValueAsString(priya);
InvalidDefinitionException: Java 8 date/time type `java.time.LocalDate` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
(through reference chain: Json$Subscriber["startedOn"])

An unusually good error message — it names the dependency you are missing. Register JavaTimeModule and add .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS), or your dates serialise as epoch numbers rather than "2026-09-01".

Spring Boot does both for you. Plain Jackson does not.

Unknown properties

By default, a field in the JSON with no matching component is an error:

UnrecognizedPropertyException: Unrecognized field "loyaltyPoints" (class Json$StrictSub),
not marked as ignorable (2 known properties: "tiffins", "name"])

That is the right default — an unexpected field usually means the API changed — but for reading someone else's API it is impractical, because they will add fields without telling you.

@JsonIgnoreProperties(ignoreUnknown = true)
record Lenient(String name, int tiffins) { }
lenient: Lenient[name=Kavita, tiffins=30]

Or globally: mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).

Be lenient about what you read, strict about what you write. For an API you consume, ignore unknowns. For your own config file, failing on a typo'd key is a feature.

The annotations worth knowing

Annotation Does
@JsonProperty("name") Maps a different JSON name
@JsonIgnore Leaves a field out entirely
@JsonIgnoreProperties(ignoreUnknown = true) Tolerates extra JSON fields
@JsonInclude(NON_NULL) Omits null fields when writing
@JsonFormat(pattern = "dd/MM/yyyy") A custom date format for one field
@JsonAlias({"qty", "quantity"}) Accepts several names when reading
@JsonCreator / @JsonValue Custom construction and representation
record Renamed(@JsonProperty("customer_name") String name,
               @JsonProperty("tiffin_count") int tiffins) { }
{"customer_name":"Priya","tiffin_count":26}
back: Renamed[name=Priya, tiffins=26]

@JsonProperty works in both directions, which is what you want for an API using snake_case while your Java uses camelCase. For a whole class, a naming strategy does it in one line instead of an annotation per field.

@JsonIgnore on anything sensitive. A password hash or a token that ends up in a response body because somebody serialised the entity directly is a real incident, and it is the argument for a separate DTO record rather than returning your domain object.

When it goes wrong

JsonEOFException: Unexpected end-of-input within/between Object entries
InvalidFormatException: Cannot deserialize value of type `int` from String "many": not a valid `int` value

All of Jackson's exceptions extend JsonProcessingException, which extends IOException — so they are checked, and module 7's advice applies: catch at a boundary and wrap with context saying which payload failed.

The messages are good. InvalidFormatException names the target type, the offending value and the field. Do not replace that with "invalid JSON".

When you do not have a class: the tree model

For a shape you do not control or do not know in advance:

JsonNode node = mapper.readTree("""
        {"order":{"id":4102,"items":[{"sku":"veg","qty":2},{"sku":"jain","qty":1}]}}
        """);

node.at("/order/id").asInt();
node.at("/order/items/0/sku").asText();
node.at("/order/nope").isMissingNode();
id       : 4102
first sku: veg
missing  : true
total qty: 3

at() takes a JSON Pointer and returns a missing node rather than null for an absent path, so a chain of them cannot throw NullPointerException — the "return empty, not null" principle from module 7, applied by a library.

Prefer mapping to a record. The tree model is for genuinely dynamic data: a webhook whose shape varies, a config file with arbitrary sections. Using it everywhere loses all the type safety this course has spent seven modules building.

Check your work

Do records need annotations to work with Jackson? No, not since Jackson 2.12. Component names become field names.

How do you deserialise a List<Subscriber>? With new TypeReference<List<Subscriber>>() {} — a class literal cannot carry the type argument through erasure.

What do you need for LocalDate to work? Register JavaTimeModule and disable WRITE_DATES_AS_TIMESTAMPS, or dates serialise as epoch numbers. The error message names the missing dependency.

What is Jackson's default for an unknown JSON property, and when should you change it? It throws UnrecognizedPropertyException. Be lenient when consuming somebody else's API; stay strict for your own config.

Which annotation stops a field being serialised at all? @JsonIgnore — and it is what keeps a password hash out of a response body.

Should you create an ObjectMapper per request? No. It is thread-safe after configuration and expensive to build. Make it a static final.

What does node.at("/a/b") return for a missing path? A missing node, not null — so chains cannot throw.

When is the tree model right? For genuinely dynamic or unknown shapes. Map to a record whenever you know the shape.

Practice 3, the date failure. Without JavaTimeModule you get InvalidDefinitionException naming java.time.LocalDate and the exact module to add. With the module registered but WRITE_DATES_AS_TIMESTAMPS still enabled, the date serialises as an array or a number rather than "2026-09-01" — it works, and produces JSON no other system will parse as a date. Both steps are needed.

Practice 5, the leaked field. Serialising a record containing a passwordHash component puts it straight into the JSON. @JsonIgnore removes it. The better fix is a separate response record containing only the fields the API should expose — because then adding a sensitive field to the domain type cannot leak it, which an annotation someone forgets to add can.

Practice

  1. Round trip a record. Serialise a Subscriber, print it pretty, read it back, and confirm equals returns true.

  2. Round trip a list. Serialise a List<Subscriber> and read it back with TypeReference. Then try it with List.class instead and look at what you actually get back.

  3. Break dates twice. Serialise a record with a LocalDate using a plain ObjectMapper and read the error. Register the module but leave WRITE_DATES_AS_TIMESTAMPS enabled, and look at the output.

  4. Handle an unknown field. Read JSON with an extra property into a strict record and read the exception. Then add @JsonIgnoreProperties(ignoreUnknown = true).

  5. Leak a secret, then stop. Put a passwordHash component on a record and serialise it. Fix it with @JsonIgnore, then again with a separate response record. Say which you would rather rely on.

  6. Harder — a config loader. Write AppConfig load(Path path) that reads a JSON config into a record with nested records, validates it in the compact constructors, and throws one exception naming the file and the offending field for any problem. Test it with a missing file, malformed JSON, a wrong type, a missing required field and an unknown field — and decide for each whether it should fail or be tolerated. That set of decisions is what a config loader actually is.


That is module eight. You can find, read and write files safely, parse real CSV and know where a hand-written parser stops, choose the right java.time type and test code that depends on the clock, and move data in and out of JSON.

Next module: concurrency — brief, and enough to recognise a race condition before you cause one.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship