Skip to main content

Jackson

Jackson with Java Records, Optionals, and Sealed Classes (Java 21+)

Modern Java — particularly Java 16 through 21 — introduced several language features that change how you model data: Records for immutable value objects, Optional for nullable return types, and Sealed Classes for closed type hierarchies. Each of these interacts with Jackson in ways that require specific setup. This guide shows you exactly what to configure so that Jackson handles all three correctly. Runnable code: every example below is in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — C01 records, C02 Optional, C03 sealed types and C04 sealed auto-discovery without @JsonSubTypes, with captured output in docs/part2-modern-java.md. Jackson with Java Records Java Records are ideal DTOs: they are immutable, concise, and carry their own equals(), hashCode(), and toString() implementations. Jackson 3 supports Records natively with zero additional modules — the canonical constructor is used automatically for deserialisation and record accessor methods replace getters for serialisation. Define a Record: // A concise, immutable data transfer object public record ProductRecord( Long productId, String productName, double unitPrice ) {}

Jackson ObjectMapper: The Complete Guide to Reading and Writing JSON in Java

Jackson is the most widely used JSON library in the Java ecosystem, and ObjectMapper is its workhorse. Whether you need to convert a Java object into a JSON string, parse a JSON file from disk, or read from an HTTP response body, ObjectMapper handles it all with a clean, consistent API. This guide covers everything you need to use Jackson’s ObjectMapper effectively in real projects. Runnable code: every example below is in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — B01 writing, B02 reading, B03 generic collections and B04 property ordering, with captured output in docs/part1-objectmapper.md. What Is Jackson ObjectMapper? ObjectMapper is the central class in Jackson’s data-binding API. It bridges the gap between Java objects (POJOs) and JSON text. Internally it uses a JsonParser for reading and a JsonGenerator for writing, but you rarely interact with those directly — ObjectMapper wraps them behind a higher-level API. Three key facts to keep in mind: Thread-safe after configuration. Create one instance, share it across your application. Recreating it per request is expensive. Highly configurable. Date formats, null handling, unknown-property behaviour, and much more are controlled via feature flags. Extensible. Custom serialisers, deserialisers, and modules let you handle any type Jackson does not know about out of the box. Adding Jackson to Your Project The jackson-databind artifact is all you need. It pulls in jackson-core and jackson-annotations as transitive dependencies.

Jackson Annotations Cheat Sheet: Every Annotation You Need With Examples

Jackson annotations give you precise control over how your Java objects map to JSON and back. Rather than relying solely on field names, you can rename properties, ignore specific fields, handle nulls, format dates, and much more — all without writing a single custom serialiser. This guide covers every annotation you will encounter in real projects, with concrete examples for each. Runnable code: every annotation below is exercised in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — D01 @JsonProperty and @JsonIgnore, D02 @JsonInclude and @JsonFormat, D03 @JsonAlias and unknown fields and D04 @JsonCreator, @JsonUnwrapped and @JsonAnyGetter, with captured output in docs/part3-annotations.md. One import note before the list: every @Json* annotation still comes from com.fasterxml.jackson.annotation, even on Jackson 3. jackson-annotations deliberately kept the old group ID and package so a single copy can serve Jackson 2 and Jackson 3 code on the same classpath — so annotated DTOs need no changes at all during a migration. @JsonProperty — Rename a Field in JSON Use @JsonProperty when the JSON key must differ from the Java field name — for example, when consuming a third-party API that uses snake_case. public class OrderSummary { @JsonProperty("order_id") // JSON key: order_id private Long orderId; @JsonProperty("customer_name") // JSON key: customer_name private String customerName; // Getters and setters } // Serialise {"order_id":1001,"customer_name":"Alice"} // Deserialise: JSON with "order_id" maps correctly to the orderId field

Jackson Custom Serialisers, Deserialisers, and Mix-in Annotations: The Advanced Toolkit

There are two scenarios where Jackson’s built-in annotations are not enough: when you need to control exactly how a complex type is serialised, and when the class you need to annotate belongs to a third-party library whose source you cannot modify. For the first case, Jackson provides custom serialisers and deserialisers. For the second, it provides Mix-in Annotations — a mechanism that lets you attach annotations to any class without touching its source. Runnable code: every example below is in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — E01 the serialiser, E02 the deserialiser, E03 module registration, E04 mix-ins and E05 ValueSerializer, with captured output in docs/part4-custom.md. This is the area with the most API churn between Jackson 2 and Jackson 3, so the renames are worth having in one place before the code: JsonSerializer → ValueSerializer, JsonDeserializer → ValueDeserializer, SerializerProvider → SerializationContext, writeNumberField/writeStringField → writeNumberProperty/writeStringProperty, parser.getCodec().readTree(parser) → ctxt.readTree(parser), mapper.registerModule(m) → builder.addModule(m), mapper.addMixIn(a, b) → builder.addMixIn(a, b), and every throws IOException deleted. StdSerializer and StdDeserializer keep their names but move to tools.jackson.databind. Writing a Custom Serialiser Extend StdSerializer<T> and override serialize(). The method receives the value to write and a JsonGenerator you use to emit JSON tokens. Suppose you have a Money type that should serialise as a structured JSON object containing the amount and the currency code: public class Money { private final BigDecimal amount; private final String currencyCode; // Constructor and getters }

Jackson 101: The Complete Java JSON Tutorial – Setup, ObjectMapper, and Best Practices

Jackson is the de facto standard for JSON processing in Java. It ships as the default JSON library in Spring Boot, is used by thousands of open-source frameworks, and handles everything from trivial string serialisation to streaming gigabyte-sized files. If you write Java and you touch JSON — which is essentially everyone — you will use Jackson. This post gives you the essential foundation: why Jackson dominates, how to set it up, and the one architectural rule that matters most. Runnable code: every example in this series lives in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21. For this post: A01 first round-trip, A02 the shared mapper and A03 the three processing models, with captured output in docs/part0-setup.md. Why Jackson, Not Gson? Both Jackson and Gson are mature, actively maintained libraries. The key differences are: FeatureJacksonGsonSpring Boot integrationBuilt-in default (zero config)Requires manual registrationPerformanceFaster on most benchmarks, especially large payloadsCompetitive on small payloadsStreaming APIYes — JsonParser / JsonGeneratorNoJava 8+ type supportVia JavaTimeModule, Jdk8ModuleLimited, requires custom adaptersPolymorphic typesFirst-class with @JsonTypeInfoRequires manual type adaptersModule ecosystemLarge (YAML, XML, CSV, CBOR, etc.)Minimal For greenfield projects on Spring Boot or Jakarta EE, Jackson is the correct default choice. Gson remains a reasonable option for Android or lightweight standalone utilities.

Jackson Polymorphic Deserialisation: Handling Inheritance Hierarchies with @JsonTypeInfo

Polymorphic deserialisation is the technique of deserialising a JSON payload into one of several possible concrete subtypes based on information embedded in the JSON itself. It is essential whenever your API works with inheritance hierarchies — event systems, payment processors, notification pipelines, or any domain where a list of heterogeneous objects needs to round-trip through JSON. Jackson handles this cleanly with @JsonTypeInfo and @JsonSubTypes. Runnable code: every example below is in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — F01 serialising a mixed list, F02 deserialising it, F03 all four include strategies and F04 bad discriminators, with captured output in docs/part5-polymorphic.md. One warning up front, because it is the single easiest way to lose an afternoon here: passing a List to writeValueAsString silently drops the type discriminator. A List carries no element type at runtime, so Jackson never engages the polymorphic type serialiser, and the resulting JSON cannot be read back. A single object serialises correctly, which is why a unit test that checks one element will not catch it. The serialisation section below shows the two fixes. The Problem: Deserialising an Abstract Type Consider an API that returns a list of payment methods. Some are credit cards, others are bank transfers. Both share a common base, but each has its own fields. Without polymorphic support, Jackson cannot decide which concrete class to instantiate when it sees a generic PaymentMethod type. Setting Up the Hierarchy with @JsonTypeInfo and @JsonSubTypes Annotate the base class (or interface) with @JsonTypeInfo to tell Jackson where the type discriminator lives in the JSON, and with @JsonSubTypes to register the permitted subtypes: @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, // Use a logical name as the discriminator include = JsonTypeInfo.As.PROPERTY, // Embed it as a field in the JSON object property = "paymentType" // The JSON key that carries the type name ) @JsonSubTypes({ @JsonSubTypes.Type(value = CreditCardPayment.class, name = "credit_card"), @JsonSubTypes.Type(value = BankTransferPayment.class, name = "bank_transfer") }) public abstract class PaymentMethod { private Long paymentId; private double amountDue; // Getters and setters }

Jackson Streaming API and JsonNode Tree Model: High-Performance JSON Processing in Java

For most applications, ObjectMapper.readValue() and writeValueAsString() are all you need. But when JSON files reach hundreds of megabytes, or when you need to work with dynamic JSON whose schema you do not know at compile time, Jackson offers two lower-level APIs that give you full control: the Streaming API (JsonParser / JsonGenerator) and the Tree Model (JsonNode). This guide explains when to use each and shows practical examples for both. Runnable code: every example below is in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — G01 the streaming filter, G02 the generator, G03 the tree model and G04 all three approaches measured on a 20 MB file, with captured output in docs/part6-streaming.md. The streaming and tree APIs took more renames than any other area in the Jackson 3 move, so here they are in one place: JsonFactory is now in tools.jackson.core.json (note the extra .json), getCurrentName() → currentName(), getText() → getString(), JsonToken.FIELD_NAME → JsonToken.PROPERTY_NAME, writeNumberField/writeStringField → writeNumberProperty/writeStringProperty, JsonNode.asText() → asString(), and createParser/createGenerator now take an ObjectReadContext/ObjectWriteContext. The Streaming API: Parsing Gigabyte-Sized JSON Files The Streaming API processes JSON one token at a time. It never loads the entire document into memory, making it the only practical choice for very large files. The trade-off is verbosity — you must walk the token stream yourself. Reading a Large JSON Array with JsonParser Suppose you have a file with millions of log entries and you only need to extract records that match a filter condition: // Jackson 3: JsonFactory is in tools.jackson.core.json - note the extra .json, // it is NOT tools.jackson.core.JsonFactory. JsonFactory jsonFactory = new JsonFactory(); int errorCount = 0; // createParser now takes an ObjectReadContext. There is no `throws IOException` // on this method in Jackson 3 - the exceptions are unchecked. try (JsonParser parser = jsonFactory.createParser( ObjectReadContext.empty(), new File("large-logs.json"))) { // Confirm the root is an array if (parser.nextToken() != JsonToken.START_ARRAY) { throw new IllegalStateException("Expected a JSON array at the root"); } // Walk each element in the array while (parser.nextToken() != JsonToken.END_ARRAY) { String logLevel = null; String logMessage = null; // Walk each field inside the current object while (parser.nextToken() != JsonToken.END_OBJECT) { String fieldName = parser.currentName(); // was getCurrentName() parser.nextToken(); // Move to field value if ("level".equals(fieldName)) { logLevel = parser.getString(); // was getText() } else if ("message".equals(fieldName)) { logMessage = parser.getString(); // was getText() } // All other fields are skipped automatically } if ("ERROR".equals(logLevel)) { System.out.println("ERROR: " + logMessage); errorCount++; } } } System.out.println("Total errors found: " + errorCount);

Jackson Security Best Practices: Defending Against Deserialization Gadget Attacks

Jackson is powerful, but some of its older configuration options can open serious security vulnerabilities in your application. The most critical is polymorphic type handling: when configured carelessly, it allows an attacker to supply a JSON payload that causes Jackson to instantiate and invoke arbitrary Java classes on your server — a Remote Code Execution (RCE) vector known as “deserialization gadget attacks.” This guide explains the risk, shows you what to avoid, and demonstrates the safe patterns you should follow in all production code. Runnable code: every example below is in the jackson3-by-example repository, compiled and executed against Jackson 3.2.1 on JDK 21 — H01 the safe pattern, H02 which APIs survived into Jackson 3, H03 a provable allowlist with a negative test, H04 resource limits and H05 Object.class, with captured output in docs/part7-security.md. The Risk: enableDefaultTyping and Gadget Attacks Jackson historically provided a method called enableDefaultTyping() that automatically embedded and honoured Java class names in serialised JSON. This means an attacker who can send JSON to your application could supply a class name of their choosing as the type discriminator, causing Jackson to instantiate that class during deserialisation. The attack exploits classes already on the JVM classpath that have side effects in their constructors or setters — so-called “gadget classes.” Common culprits include JDBC drivers, logging libraries, and many Apache Commons classes. Never use this in production: // DANGEROUS — do not use in any application that receives untrusted JSON mapper.enableDefaultTyping(); // Deprecated and removed in Jackson 2.16 mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // Also dangerous Both were deprecated in Jackson 2.10 and are completely absent in Jackson 3.x. If you encounter them in a codebase being migrated to Jackson 3, treat it as a blocker — the code will not compile and the security risk must be remediated before upgrading. One correction worth making here, since it changes when the pain lands: enableDefaultTyping() was not removed in 2.16. It is still present, deprecated, on ObjectMapper in Jackson 2.22.1 — a reflective check confirms it. So a Jackson 2 codebase can keep compiling against it through every 2.x upgrade, and Jackson 3 is where it finally fails. That is arguably the removal working as intended: the compiler forces the audit exactly once, at the major version boundary.