Jackson ObjectMapper Configuration: Features, Builders & ObjectReader/Writer
ObjectMapper is the class at the centre of Jackson: it turns Java objects into JSON and back. How it finds properties, and its defaults for dates, unknown fields, nulls and errors, decide most of what goes wrong with JSON in Java. This page explains how the mapper works, runs each default and feature with Jackson 2.21, measures the cost of creating mappers, and shows what changed in Jackson 3.
Contents
How ObjectMapper works
Jackson is built in three layers. The bottom layer, jackson-coreJsonParserSTART_OBJECTFIELD_NAMEVALUE_NUMBER_INTJsonGeneratorjackson-databindJsonNodeObjectMapper
When a mapper meets a class for the first time, it introspects it: it looks for public getters, setters and fields, record components, constructors marked as creators, and annotations. From that it builds a serializer or deserializer for the class and caches it inside the mapper. Every later call for the same class reuses the cached object, which is why a mapper is expensive to create and cheap to use. A configured mapper is thread-safe, so an application normally has one, or a few with different settings.
| API | Since | Best for |
|---|---|---|
new ObjectMapper() | Jackson 1 | Quick use with default settings |
JsonMapper.builder() | Jackson 2.10 | Configuring a mapper once, then never changing it |
ObjectReaderObjectWriter | Jackson 1.x | Per-use variations: a target type, pretty printing, a view |
readTreeJsonNode | Jackson 1 | JSON whose shape you do not know in advance; see the |
JsonParserJsonGenerator | Jackson 1 | Very large documents, processed token by token |
tools.jackson.databind.json.JsonMapper | Jackson 3.0 (2025) | New projects and Spring Boot 4, with immutable mappers and better defaults |
Most examples use Jackson 2.21 (jackson-databind
How Jackson finds properties
The program serializes a classic JavaBean with getters and setters, a record, and a class with only a one-argument constructor. The bean has a derived getFullName()
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Basics {
// A classic JavaBean: Jackson finds properties through public getters and setters
public static class Customer {
private String first;
private String last;
private boolean active;
private String internalNote = "not exposed"; // no getter, so not serialized
public Customer() {
}
public Customer(String first, String last, boolean active) {
this.first = first;
this.last = last;
this.active = active;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public String getFullName() { // derived getter: becomes a property
return first + " " + last;
}
String note() {
return internalNote;
}
}
// A record: Jackson 2.12+ uses the components and the canonical constructor
record Point(int x, int y) {
}
// No default constructor and no creator annotation
public static class Money {
private final long cents;
public Money(long cents) {
this.cents = cents;
}
public long getCents() {
return cents;
}
}
// The fix: tell Jackson which constructor to call and how parameters map to JSON
public static class Money2 {
private final long cents;
@JsonCreator
public Money2(@JsonProperty("cents") long cents) {
this.cents = cents;
}
public long getCents() {
return cents;
}
}
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(new Customer("Ana", "Silva", true));
System.out.println("bean -> " + json);
Customer back = mapper.readValue(
"{\"first\":\"Ben\",\"last\":\"Ode\",\"active\":false}", Customer.class);
System.out.println("bean <- " + back.getFullName() + ", active=" + back.isActive()
+ ", note=" + back.note());
String pj = mapper.writeValueAsString(List.of(new Point(1, 2), new Point(3, 4)));
System.out.println("record -> " + pj);
Point[] points = mapper.readValue(pj, Point[].class);
System.out.println("record <- " + points[1]);
// Writing to a file and reading bytes back
Path file = Files.createTempFile("customer", ".json");
mapper.writeValue(file.toFile(), new Customer("Cho", "Kim", true));
byte[] bytes = Files.readAllBytes(file);
System.out.println("file has " + bytes.length + " bytes: " + new String(bytes,
StandardCharsets.UTF_8));
try {
mapper.readValue(bytes, Customer.class); // fullName has no setter
} catch (UnrecognizedPropertyException e) {
System.out.println("read back: UnrecognizedPropertyException for \""
+ e.getPropertyName() + "\", known " + e.getKnownPropertyIds());
}
Files.delete(file);
// Serializing works, deserializing needs a way to construct the object
System.out.println("Money -> " + mapper.writeValueAsString(new Money(1999)));
try {
mapper.readValue("{\"cents\":1999}", Money.class);
} catch (MismatchedInputException e) {
System.out.println("Money <- " + e.getOriginalMessage());
}
System.out.println("Money2 <- cents=" + mapper.readValue("{\"cents\":1999}",
Money2.class).getCents());
}
}
Output:
bean -> {"first":"Ana","last":"Silva","active":true,"fullName":"Ana Silva"}
bean <- Ben Ode, active=false, note=not exposed
record -> [{"x":1,"y":2},{"x":3,"y":4}]
record <- Point[x=3, y=4]
file has 63 bytes: {"first":"Cho","last":"Kim","active":true,"fullName":"Cho Kim"}
read back: UnrecognizedPropertyException for "fullName", known [first, last, active]
Money -> {"cents":1999}
Money <- Cannot construct instance of `Basics$Money` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
Money2 <- cents=1999
- The JSON property names come from the getters, not the fields:
becomesgetFirst() and the booleanfirst becomesisActive() . The privateactive field has no getter and is not written. Jackson by default sees public fields, public getters, and setters of any visibility.internalNote has no field behind it, yet it appears asgetFullName() , because any public getter is a property. That is the trap in the file round trip: the 63-byte file contains"fullName":"Ana Silva" , and reading it back throwsfullName , because there is no setter for it and the known properties are onlyUnrecognizedPropertyException . Mark such getters with[first, last, active] , or allow the property on read with@JsonIgnore .@JsonIgnoreProperties(value = "fullName", allowGetters = true)- Records work in both directions since Jackson 2.12: the components are the properties, and the canonical constructor is used for reading. The array of two points came back as
.Point[x=3, y=4] serializes fine asMoney , but reading fails with "no delegate- or property-based Creator". Java does not keep constructor parameter names in the class file unless you compile with{"cents":1999} , so Jackson cannot know that the-parameters argument is calledlong .cents with@JsonCreator on the parameter fixes it, as@JsonProperty("cents") shows. TheMoney2 module plusjackson-module-parameter-names is the other fix; see-parametersJackson modules .
The defaults that surprise people
The program serializes an order record with a LocalDate
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.LocalDate;
import java.util.List;
public class MapperBasics {
record Order(long id, String customer, List<String> items, LocalDate placed) {
}
record Product(String sku, double price) {
}
public static void main(String[] args) throws Exception {
// Out of the box, Jackson 2 cannot handle java.time types
ObjectMapper plain = new ObjectMapper();
Order order = new Order(42, "Ana", List.of("pen", "pad"), LocalDate.of(2026, 1, 15));
try {
plain.writeValueAsString(order);
} catch (InvalidDefinitionException e) {
System.out.println("no JavaTimeModule: InvalidDefinitionException");
}
// With the module, dates are written as arrays unless that feature is turned off
ObjectMapper withTime = JsonMapper.builder().addModule(new JavaTimeModule()).build();
System.out.println("module only: " + withTime.writeValueAsString(order));
ObjectMapper iso = JsonMapper.builder().addModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
String json = iso.writeValueAsString(order);
System.out.println("ISO dates: " + json);
System.out.println("round trip equal: " + iso.readValue(json, Order.class).equals(order));
// Unknown properties fail by default
String newerApi = "{\"sku\":\"A1\",\"price\":9.5,\"currency\":\"EUR\"}";
try {
plain.readValue(newerApi, Product.class);
} catch (UnrecognizedPropertyException e) {
System.out.println("unknown \"" + e.getPropertyName()
+ "\": UnrecognizedPropertyException");
}
ObjectMapper lenient = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
System.out.println("lenient mapper: " + lenient.readValue(newerApi, Product.class));
// Missing primitive fields silently become 0 unless this feature is on
String missingPrice = "{\"sku\":\"B2\"}";
System.out.println("missing price: " + plain.readValue(missingPrice, Product.class));
ObjectMapper strict = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES).build();
try {
strict.readValue("{\"sku\":\"B2\",\"price\":null}", Product.class);
} catch (Exception e) {
System.out.println("null for a double with FAIL_ON_NULL_FOR_PRIMITIVES: "
+ e.getClass().getSimpleName());
}
}
}
Output:
no JavaTimeModule: InvalidDefinitionException
module only: {"id":42,"customer":"Ana","items":["pen","pad"],"placed":[2026,1,15]}
ISO dates: {"id":42,"customer":"Ana","items":["pen","pad"],"placed":"2026-01-15"}
round trip equal: true
unknown "currency": UnrecognizedPropertyException
lenient mapper: Product[sku=A1, price=9.5]
missing price: Product[sku=B2, price=0.0]
null for a double with FAIL_ON_NULL_FOR_PRIMITIVES: MismatchedInputException
| Situation | Default | Change with |
|---|---|---|
java.time | Error: InvalidDefinitionException | addModule(new JavaTimeModule()) |
| Dates with the module | Arrays such as [2026,1,15] | disable(WRITE_DATES_AS_TIMESTAMPS) |
| Unknown JSON property | Error: UnrecognizedPropertyException | disable(FAIL_ON_UNKNOWN_PROPERTIES) |
| Missing primitive property | Silently 0 or false | Validate after reading, or use wrapper types |
Explicit null | Silently 0 or false | enable(FAIL_ON_NULL_FOR_PRIMITIVES) |
Failing on unknown properties is safe while two systems are developed together, but a client that reads another team's API usually wants to ignore new fields, so it does not break when the API grows. The missing price that came back as 0.0 is the most dangerous default here: nothing signals that the value was absent. A record with an IntegerBigDecimalnull[2026,1,15]
Configuration features
Settings live in four enums: SerializationFeatureDeserializationFeatureMapperFeaturejackson-core
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Features {
enum Status { ACTIVE, SUSPENDED }
record Account(String userName, Status status, int loginCount, List<String> roles) {
}
static class Empty {
}
public static void main(String[] args) throws Exception {
Account acc = new Account("ana", Status.ACTIVE, 3, List.of("admin"));
ObjectMapper plain = new ObjectMapper();
System.out.println("default: " + plain.writeValueAsString(acc));
// Naming strategy and alphabetical order, set once on the builder
ObjectMapper snake = JsonMapper.builder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build();
System.out.println("snake + sorted: " + snake.writeValueAsString(acc));
// Map keys keep insertion order unless ORDER_MAP_ENTRIES_BY_KEYS is on
Map<String, Integer> scores = new java.util.LinkedHashMap<>();
scores.put("zoe", 1);
scores.put("adam", 2);
ObjectMapper ordered = JsonMapper.builder()
.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS).build();
System.out.println("map default: " + plain.writeValueAsString(scores));
System.out.println("map ordered: " + ordered.writeValueAsString(scores));
System.out.println("TreeMap equal: " + ordered.writeValueAsString(new TreeMap<>(scores))
.equals(ordered.writeValueAsString(scores)));
// An object with no properties fails by default
try {
plain.writeValueAsString(new Empty());
} catch (Exception e) {
System.out.println("empty bean: " + e.getClass().getSimpleName());
}
ObjectMapper allowEmpty = JsonMapper.builder()
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS).build();
System.out.println("empty allowed: " + allowEmpty.writeValueAsString(new Empty()));
// Lenient reading: a single value where an array is expected, case, unknown enums
String loose = "{\"USERNAME\":\"ben\",\"status\":\"DELETED\",\"loginCount\":1,"
+ "\"roles\":\"user\"}";
try {
plain.readValue(loose, Account.class);
} catch (JsonMappingException e) {
System.out.println("strict read: " + e.getClass().getSimpleName() + " at "
+ e.getPathReference());
}
ObjectMapper lenient = JsonMapper.builder()
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
.build();
System.out.println("lenient read: " + lenient.readValue(loose, Account.class));
// Floating point numbers in untyped JSON
String price = "{\"price\":0.1000000000000000055511151231257827}";
System.out.println("as Double: " + plain.readValue(price, Map.class));
ObjectMapper exact = JsonMapper.builder()
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS).build();
Map<?, ?> m = exact.readValue(price, Map.class);
System.out.println("as BigDecimal: " + m + " (" + m.get("price").getClass()
.getSimpleName() + ")");
// rebuild() starts a new builder from an existing mapper's settings
ObjectMapper pretty = ((JsonMapper) snake).rebuild()
.enable(SerializationFeature.INDENT_OUTPUT).build();
System.out.println("rebuilt + INDENT_OUTPUT:\n" + pretty.writeValueAsString(acc));
System.out.println("original unchanged: "
+ !snake.isEnabled(SerializationFeature.INDENT_OUTPUT));
}
}
Output:
default: {"userName":"ana","status":"ACTIVE","loginCount":3,"roles":["admin"]}
snake + sorted: {"login_count":3,"roles":["admin"],"status":"ACTIVE","user_name":"ana"}
map default: {"zoe":1,"adam":2}
map ordered: {"adam":2,"zoe":1}
TreeMap equal: true
empty bean: InvalidDefinitionException
empty allowed: {}
strict read: InvalidFormatException at Features$Account["status"]
lenient read: Account[userName=ben, status=null, loginCount=1, roles=[user]]
as Double: {price=0.1}
as BigDecimal: {price=0.1000000000000000055511151231257827} (BigDecimal)
rebuilt + INDENT_OUTPUT:
{
"login_count" : 3,
"roles" : [ "admin" ],
"status" : "ACTIVE",
"user_name" : "ana"
}
original unchanged: true
turnedPropertyNamingStrategies.SNAKE_CASE intouserName anduser_name intologinCount , andlogin_count put them in name order. Without it, Jackson 2 writes properties in declaration order.SORT_PROPERTIES_ALPHABETICALLY- Map keys keep the map's own order (the
gave zoe before adam) unlessLinkedHashMap is on. Sorted output is useful when JSON is compared as text, for example in tests or when computing a signature.ORDER_MAP_ENTRIES_BY_KEYS - A class with no visible properties fails with
, because writingInvalidDefinitionException for it is usually a mistake, such as forgotten getters.{} turns that off.FAIL_ON_EMPTY_BEANS - The loose input has three problems: an upper-case
, an enum valueUSERNAME that does not exist, and a single string where a list is expected. The strict mapper stopped at the first one it checked,DELETED . The lenient mapper accepted all three: the name matched case-insensitively, the unknown enum becamestatus , andnull became"user" .[user] - Untyped JSON numbers with a fraction become
, so 34 significant digits were cut toDouble .0.1 kept all of them in aUSE_BIG_DECIMAL_FOR_FLOATS . Typed targets do not need it: aBigDecimal field always receives the exact text; seeBigDecimalBigDecimal . creates a builder pre-filled with an existing mapper's settings, so a pretty-printing variant can be derived without touching the original, which still reportsrebuild() off.INDENT_OUTPUT
Jackson 2 also lets you change a live mapper with configure(...)enable(...)setSerializationInclusion(...)
ObjectReader, ObjectWriter and other mapper methods
writer...()reader...()convertValuereaderForUpdating
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.json.JsonMapper;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
public class ReadersWriters {
record User(String name, String email, Integer age) {
}
public static class Settings {
public String theme = "light";
public int fontSize = 12;
}
// One configured mapper for the whole application
static final ObjectMapper MAPPER = JsonMapper.builder()
.defaultPropertyInclusion(JsonInclude.Value.construct(
JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))
.build();
public static void main(String[] args) throws Exception {
User ana = new User("Ana", null, 30);
// ObjectWriter: an immutable, per-use variation of the mapper's settings
ObjectWriter pretty = MAPPER.writerWithDefaultPrettyPrinter();
System.out.println("compact: " + MAPPER.writeValueAsString(ana));
System.out.println("pretty:\n" + pretty.writeValueAsString(ana));
// ObjectReader bound to a generic type, created once and reused
ObjectReader listReader = MAPPER.readerFor(new TypeReference<List<User>>() { });
List<User> users = listReader.readValue(
"[{\"name\":\"Ben\",\"age\":25},{\"name\":\"Cho\",\"email\":\"c@x.io\"}]");
System.out.println("list of records: " + users);
// Without type information, JSON becomes Maps, Lists and boxed numbers
Object untyped = MAPPER.readValue("{\"a\":1,\"b\":[true,2.5]}", Object.class);
System.out.println("untyped: " + untyped + " as " + untyped.getClass().getSimpleName());
Map<?, ?> map = (Map<?, ?>) untyped;
System.out.println(" \"a\" is " + map.get("a").getClass().getSimpleName());
// The configured mapper is thread-safe: use it from many threads at once
long ok = IntStream.range(0, 10_000).parallel().filter(i -> {
try {
User u = MAPPER.readValue(MAPPER.writeValueAsString(new User("u" + i, null, i)),
User.class);
return u.age() == i;
} catch (Exception e) {
return false;
}
}).count();
System.out.println("parallel round trips correct: " + ok + " of 10000");
// convertValue: object to object through Jackson's model, without JSON text
Map<?, ?> asMap = MAPPER.convertValue(ana, Map.class);
System.out.println("convertValue to Map: " + asMap);
User fromMap = MAPPER.convertValue(Map.of("name", "Dee", "age", 41), User.class);
System.out.println("convertValue to User: " + fromMap);
// readerForUpdating: apply a partial JSON document to an existing object
Settings settings = new Settings();
MAPPER.readerForUpdating(settings).readValue("{\"theme\":\"dark\"}");
System.out.println("updated settings: theme=" + settings.theme + ", fontSize="
+ settings.fontSize);
}
}
Output:
compact: {"name":"Ana","age":30}
pretty:
{
"name" : "Ana",
"age" : 30
}
list of records: [User[name=Ben, email=null, age=25], User[name=Cho, email=c@x.io, age=null]]
untyped: {a=1, b=[true, 2.5]} as LinkedHashMap
"a" is Integer
parallel round trips correct: 10000 of 10000
convertValue to Map: {name=Ana, age=30}
convertValue to User: User[name=Dee, email=null, age=41]
updated settings: theme=dark, fontSize=12
- With
inclusion, Ana's null email is left out of the JSON rather than written asNON_NULL . Cho's missing"email":null was read asage because the component is annull .Integer - A
keeps the generic typeTypeReference that erasure would otherwise lose. PassingList<User> instead gives a list ofList.class s; seeLinkedHashMapJackson modules and type handling . - Read as
, JSON becomesObject ,LinkedHashMap ,ArrayList ,Integer andDouble . For untyped JSON, theBooleantree model is usually easier to work with. - One mapper served 10,000 parallel round trips with every result correct. Sharing a configured
,ObjectMapper orObjectReader between threads needs no locking.ObjectWriter maps one Java object onto another type through Jackson's own model, without producing JSON text: the record becameconvertValue (still respecting{name=Ana, age=30} ), and aNON_NULL became aMap .User applied a partial document to an existing object:readerForUpdating changed to dark andtheme kept its value of 12. This is a simple way to implement a merge-style partial update, such as an HTTP PATCH endpoint.fontSize
Reading error messages
Jackson exceptions carry a location and a property path, and both are easy to throw away by logging only the class name. The program reads three bad documents into an order with a list of lines: broken syntax, a wrong value type deep in the list, and an array where an object is expected.
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import java.util.List;
public class Errors {
record Line(String sku, int qty) {
}
record Order(long id, List<Line> lines) {
}
static void tryRead(ObjectMapper mapper, String label, String json) {
try {
mapper.readValue(json, Order.class);
System.out.println(label + ": ok");
} catch (JsonProcessingException e) {
System.out.println(label + ": " + e.getClass().getSimpleName());
System.out.println(" message: " + e.getOriginalMessage().lines().findFirst()
.orElse(""));
System.out.println(" location: line " + e.getLocation().getLineNr() + ", column "
+ e.getLocation().getColumnNr());
if (e instanceof JsonMappingException jme) {
String path = jme.getPathReference();
System.out.println(" path: " + (path.isEmpty() ? "(root)" : path));
}
}
}
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
tryRead(mapper, "missing comma", "{\"id\":1 \"lines\":[]}");
tryRead(mapper, "wrong type ", """
{"id": 7,
"lines": [{"sku": "A1", "qty": 2},
{"sku": "B2", "qty": "two"}]}""");
tryRead(mapper, "array for object", "[1, 2]");
// Since Jackson 2.16 the source text is left out of messages by default
ObjectMapper withSource = JsonMapper.builder()
.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build();
try {
withSource.readValue("{\"id\":true}", Order.class);
} catch (JsonProcessingException e) {
String full = e.getMessage();
System.out.println("with source: " + full.substring(full.indexOf("[Source")));
}
// The exception hierarchy: all are IOExceptions
for (Class<?> c = com.fasterxml.jackson.databind.exc.InvalidFormatException.class;
c != Exception.class; c = c.getSuperclass()) {
System.out.print(c.getSimpleName() + " > ");
}
System.out.println("Exception");
System.out.println("JsonParseException is a JsonMappingException: "
+ JsonMappingException.class.isAssignableFrom(JsonParseException.class));
}
}
Output:
missing comma: JsonParseException
message: Unexpected character ('"' (code 34)): was expecting comma to separate Object entries
location: line 1, column 9
wrong type : InvalidFormatException
message: Cannot deserialize value of type `int` from String "two": not a valid `int` value
location: line 3, column 33
path: Errors$Order["lines"]->java.util.ArrayList[1]->Errors$Line["qty"]
array for object: MismatchedInputException
message: Cannot deserialize value of type `Errors$Order` from Array value (token `JsonToken.START_ARRAY`)
location: line 1, column 1
path: (root)
with source: [Source: (String)"{"id":true}"; line: 1, column: 7] (through reference chain: Errors$Order["id"])
InvalidFormatException > MismatchedInputException > JsonMappingException > DatabindException > JsonProcessingException > JacksonException > IOException > Exception
JsonParseException is a JsonMappingException: false
- A missing comma is a syntax error, found by the streaming parser, so it is a
with a line and column (column 9 is the quote afterJsonParseException ) but no property path.1 for an"two" is a data-binding error,int . Its path,InvalidFormatException , names the exact element: the second line's quantity, on line 3 of the input. Return this path in a 400 response and API clients can fix their request without guessing.Order["lines"]->ArrayList[1]->Line["qty"]- A JSON array where the root type is a record is a
at the root, so the path is empty.MismatchedInputException - Since Jackson 2.16, messages show
instead of the input, because the input may contain passwords or personal data that would end up in logs.REDACTED brings the source back, hereStreamReadFeature.INCLUDE_SOURCE_IN_LOCATION ; enable it only for debugging.(String)"{"id":true}" - All of these exceptions extend
, which in Jackson 2 is a checkedJsonProcessingException .IOException is a separate branch fromJsonParseException , so catchJsonMappingException to handle both.JsonProcessingException
Why the mapper must be shared: a measurement
Because introspection results are cached per mapper, a new ObjectMapperObjectWriterObjectReader
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import java.util.List;
public class MapperSpeed {
record Item(String sku, int qty, double price) {
}
record Order(long id, String customer, List<Item> items) {
}
interface Task {
void run() throws Exception;
}
static final int N = 20_000;
static long sink;
static long bestOf3(Task task) throws Exception {
long best = Long.MAX_VALUE;
task.run(); // warm-up run, not counted
for (int run = 0; run < 3; run++) {
long t0 = System.nanoTime();
task.run();
best = Math.min(best, System.nanoTime() - t0);
}
return best;
}
public static void main(String[] args) throws Exception {
Order order = new Order(42, "Ana", List.of(new Item("A1", 2, 9.5), new Item("B2", 1, 3)));
ObjectMapper shared = new ObjectMapper();
ObjectWriter writer = shared.writerFor(Order.class);
ObjectReader reader = shared.readerFor(Order.class);
String json = shared.writeValueAsString(order);
System.out.println(json.length() + "-char order, " + N + " operations per run");
long fresh = bestOf3(() -> {
for (int i = 0; i < N / 10; i++) { // 10x fewer: it is slow
sink += new ObjectMapper().writeValueAsString(order).length();
}
}) * 10;
long reuse = bestOf3(() -> {
for (int i = 0; i < N; i++) {
sink += shared.writeValueAsString(order).length();
}
});
long viaWriter = bestOf3(() -> {
for (int i = 0; i < N; i++) {
sink += writer.writeValueAsString(order).length();
}
});
long read = bestOf3(() -> {
for (int i = 0; i < N; i++) {
sink += shared.readValue(json, Order.class).items().size();
}
});
long viaReader = bestOf3(() -> {
for (int i = 0; i < N; i++) {
sink += reader.<Order>readValue(json).items().size();
}
});
System.out.printf("write, new mapper each time: %6.0f ns per call%n", fresh / (double) N);
System.out.printf("write, shared mapper: %6.0f ns per call%n", reuse / (double) N);
System.out.printf("write, shared ObjectWriter: %6.0f ns per call%n",
viaWriter / (double) N);
System.out.printf("read, shared mapper: %6.0f ns per call%n", read / (double) N);
System.out.printf("read, shared ObjectReader: %6.0f ns per call%n",
viaReader / (double) N);
System.out.printf("new mapper is %.0fx slower than reuse%n", fresh / (double) reuse);
if (sink == 0) {
System.out.println("nothing was measured");
}
}
}
Output of the mapper timing run:
102-char order, 20000 operations per run
write, new mapper each time: 90146 ns per call
write, shared mapper: 1199 ns per call
write, shared ObjectWriter: 921 ns per call
read, shared mapper: 1565 ns per call
read, shared ObjectReader: 1002 ns per call
new mapper is 75x slower than reuse
- Creating a mapper for each call was tens of times slower than reusing one; across several runs on this machine the ratio ranged from about 60 to over 100. Almost all of that time goes into creating the mapper and introspecting
andOrder again, not into writing 102 characters.Item - The prebuilt
andObjectWriter were faster than the equivalent mapper calls, because they look up the serializer or deserializer for the type once, when created, instead of on every call. The size of that gain varied a lot between runs, so treat it as a bonus rather than a reason to restructure code.ObjectReader - The practical rule: create mappers at start-up and keep them in a
field or a dependency injection bean (a Spring Boot application already has one). Use readers and writers for variations instead of new mappers.static final
Jackson 3
Jackson 3.0 was released in 2025 with a new package name, tools.jacksoncom.fasterxml.jackson.annotation
import java.time.LocalDate;
import java.util.List;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
// The same calls with Jackson 3 (package tools.jackson)
public class Jackson3 {
record Order(long id, String customer, List<String> items, LocalDate placed) {
}
record Product(String sku, double price) {
}
public static class Bean {
public int zeta = 1;
public int alpha = 2;
}
static void read(ObjectMapper mapper, String label, String json) {
try {
System.out.println(label + mapper.readValue(json, Product.class));
} catch (JacksonException e) {
System.out.println(label + e.getClass().getSimpleName());
}
}
public static void main(String[] args) {
ObjectMapper mapper = JsonMapper.builder().build();
Order order = new Order(42, "Ana", List.of("pen", "pad"), LocalDate.of(2026, 1, 15));
// java.time support is built in, and dates are ISO strings by default
String json = mapper.writeValueAsString(order); // no checked exception
System.out.println("Jackson 3 default: " + json);
System.out.println("round trip equal: " + mapper.readValue(json, Order.class)
.equals(order));
System.out.println("field order: " + mapper.writeValueAsString(new Bean()));
read(mapper, "unknown property: ", "{\"sku\":\"A1\",\"price\":9.5,\"currency\":\"EUR\"}");
read(mapper, "missing price: ", "{\"sku\":\"B2\"}");
read(mapper, "null price: ", "{\"sku\":\"B2\",\"price\":null}");
System.out.println("FAIL_ON_NULL_FOR_PRIMITIVES on: "
+ mapper.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES));
// Mappers are immutable; rebuild() gives a builder with the same settings
ObjectMapper pretty = mapper.rebuild().enable(SerializationFeature.INDENT_OUTPUT).build();
System.out.println("rebuilt mapper indents: "
+ pretty.writeValueAsString(new Product("C3", 1.0)).contains("\n"));
System.out.println("JacksonException is unchecked: "
+ RuntimeException.class.isAssignableFrom(JacksonException.class));
}
}
Output (Jackson 3.1.7):
Jackson 3 default: {"id":42,"customer":"Ana","items":["pen","pad"],"placed":"2026-01-15"}
round trip equal: true
field order: {"alpha":2,"zeta":1}
unknown property: Product[sku=A1, price=9.5]
missing price: MismatchedInputException
null price: MismatchedInputException
FAIL_ON_NULL_FOR_PRIMITIVES on: true
rebuilt mapper indents: true
JacksonException is unchecked: true
| Behavior | Jackson 2.21 | Jackson 3.1 |
|---|---|---|
java.time | Needs JavaTimeModule | Built in |
| Date format | Timestamps (arrays) | ISO-8601 strings |
| Unknown properties | Fail | Ignored |
| Missing or null primitive | Silently 0 | Fails: FAIL_ON_NULL_FOR_PRIMITIVES |
| Property order | Declaration order | Alphabetical |
| Exceptions | Checked JsonProcessingException | Unchecked JacksonException |
| Configuration | Builder or mutable mapper | Builder only; rebuild() |
The missing price, which Jackson 2 silently turned into 0.0, now fails, and so does an explicit nullzetaalpha
Choosing settings and APIs
| Need | Use |
|---|---|
| Application-wide mapper | One JsonMapper.builder()...build() |
| Pretty printing for one call | mapper.writerWithDefaultPrettyPrinter() |
Reading a generic type such as List<User> | mapper.readerFor(new TypeReference<List<User>>() { }) |
| Reading another team's API | disable(FAIL_ON_UNKNOWN_PROPERTIES) |
| Dates as text | JavaTimeModuledisable(WRITE_DATES_AS_TIMESTAMPS) |
| Detecting absent numbers | Wrapper types, or FAIL_ON_NULL_FOR_PRIMITIVES |
Object to Map | convertValue |
| Partial update of an object | readerForUpdating(target) |
| Unknown or changing JSON shape | readTree |
| Different JSON per class without changing it |