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.

Share

Contents

How ObjectMapper works

Jackson is built in three layers. The bottom layer, jackson-core, is a streaming parser and generator: JsonParser reads JSON as a sequence of tokens such as START_OBJECT, FIELD_NAME and VALUE_NUMBER_INT, and JsonGenerator writes them. Above it, jackson-databind provides two ways to work with whole documents: the tree model of JsonNode objects, and data binding, which maps JSON directly onto your own classes. ObjectMapper is the entry point to both.

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.

The main Jackson entry points
APISinceBest for
new ObjectMapper()Jackson 1Quick use with default settings
JsonMapper.builder()Jackson 2.10Configuring a mapper once, then never changing it
ObjectReader, ObjectWriterJackson 1.xPer-use variations: a target type, pretty printing, a view
readTree and JsonNodeJackson 1JSON whose shape you do not know in advance; see the tree model
JsonParser, JsonGeneratorJackson 1Very large documents, processed token by token
tools.jackson.databind.json.JsonMapperJackson 3.0 (2025)New projects and Spring Boot 4, with immutable mappers and better defaults

Most examples use Jackson 2.21 (jackson-databind 2.21.4) on JDK 21; the Jackson 3 section uses 3.1.7. Every program is complete and needs only the Jackson jars on the classpath.

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() getter and a private field without a getter, because those are the two cases where what Jackson sees differs from what the class contains.

Java
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:

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: getFirst() becomes first and the boolean isActive() becomes active. The private internalNote field has no getter and is not written. Jackson by default sees public fields, public getters, and setters of any visibility.
  • getFullName() has no field behind it, yet it appears as "fullName":"Ana Silva", because any public getter is a property. That is the trap in the file round trip: the 63-byte file contains fullName, and reading it back throws UnrecognizedPropertyException, because there is no setter for it and the known properties are only [first, last, active]. Mark such getters with @JsonIgnore, or allow the property on read with @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].
  • Money serializes fine as {"cents":1999}, 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 -parameters, so Jackson cannot know that the long argument is called cents. @JsonCreator with @JsonProperty("cents") on the parameter fixes it, as Money2 shows. The jackson-module-parameter-names module plus -parameters is the other fix; see Jackson modules.

The defaults that surprise people

The program serializes an order record with a LocalDate, then reads JSON that has an extra field and JSON that lacks a field. Each case runs first with a default mapper and then with the setting that changes it. These are the defaults behind most Jackson questions in code reviews.

Java
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:

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
Jackson 2 defaults shown above
SituationDefaultChange with
java.time typesError: InvalidDefinitionExceptionaddModule(new JavaTimeModule())
Dates with the moduleArrays such as [2026,1,15]disable(WRITE_DATES_AS_TIMESTAMPS) for ISO strings
Unknown JSON propertyError: UnrecognizedPropertyExceptiondisable(FAIL_ON_UNKNOWN_PROPERTIES)
Missing primitive propertySilently 0 or falseValidate after reading, or use wrapper types
Explicit null for a primitiveSilently 0 or falseenable(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 Integer or BigDecimal component gets null instead, which can be checked. The date array [2026,1,15] is valid JSON but rarely what a JavaScript client expects, which is why most projects turn timestamps off.

Configuration features

Settings live in four enums: SerializationFeature for writing, DeserializationFeature for reading, MapperFeature for introspection, and the stream features of jackson-core. The program uses an account record with camel-case names, an enum and a list, because naming strategies, enum handling and single-value arrays all show up on such a type.

Java
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:

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
  • PropertyNamingStrategies.SNAKE_CASE turned userName into user_name and loginCount into login_count, and SORT_PROPERTIES_ALPHABETICALLY put them in name order. Without it, Jackson 2 writes properties in declaration order.
  • Map keys keep the map's own order (the LinkedHashMap gave zoe before adam) unless ORDER_MAP_ENTRIES_BY_KEYS is on. Sorted output is useful when JSON is compared as text, for example in tests or when computing a signature.
  • A class with no visible properties fails with InvalidDefinitionException, because writing {} for it is usually a mistake, such as forgotten getters. FAIL_ON_EMPTY_BEANS turns that off.
  • The loose input has three problems: an upper-case USERNAME, an enum value DELETED that does not exist, and a single string where a list is expected. The strict mapper stopped at the first one it checked, status. The lenient mapper accepted all three: the name matched case-insensitively, the unknown enum became null, and "user" became [user].
  • Untyped JSON numbers with a fraction become Double, so 34 significant digits were cut to 0.1. USE_BIG_DECIMAL_FOR_FLOATS kept all of them in a BigDecimal. Typed targets do not need it: a BigDecimal field always receives the exact text; see BigDecimal.
  • rebuild() 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 reports INDENT_OUTPUT off.

Jackson 2 also lets you change a live mapper with configure(...), enable(...) and setSerializationInclusion(...). That is only safe before the mapper is first used, because other threads may already have cached serializers built with the old settings. The builder makes the question disappear: the mapper it returns is fully configured from the start.

ObjectReader, ObjectWriter and other mapper methods

writer...() and reader...() return immutable, preconfigured views of a mapper for one use, such as pretty printing or a fixed target type. The program uses them on a user record whose email may be null, reads JSON without a target type, checks thread safety under load, and shows two methods people often reimplement by hand: convertValue and readerForUpdating.

Java
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:

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 NON_NULL inclusion, Ana's null email is left out of the JSON rather than written as "email":null. Cho's missing age was read as null because the component is an Integer.
  • A TypeReference keeps the generic type List<User> that erasure would otherwise lose. Passing List.class instead gives a list of LinkedHashMaps; see Jackson modules and type handling.
  • Read as Object, JSON becomes LinkedHashMap, ArrayList, Integer, Double and Boolean. For untyped JSON, the tree model is usually easier to work with.
  • One mapper served 10,000 parallel round trips with every result correct. Sharing a configured ObjectMapper, ObjectReader or ObjectWriter between threads needs no locking.
  • convertValue maps one Java object onto another type through Jackson's own model, without producing JSON text: the record became {name=Ana, age=30} (still respecting NON_NULL), and a Map became a User.
  • readerForUpdating applied a partial document to an existing object: theme changed to dark and fontSize kept its value of 12. This is a simple way to implement a merge-style partial update, such as an HTTP PATCH endpoint.

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.

Java
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:

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 JsonParseException with a line and column (column 9 is the quote after 1) but no property path.
  • "two" for an int is a data-binding error, InvalidFormatException. Its path, Order["lines"]->ArrayList[1]->Line["qty"], 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.
  • A JSON array where the root type is a record is a MismatchedInputException at the root, so the path is empty.
  • Since Jackson 2.16, messages show REDACTED instead of the input, because the input may contain passwords or personal data that would end up in logs. StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION brings the source back, here (String)"{"id":true}"; enable it only for debugging.
  • All of these exceptions extend JsonProcessingException, which in Jackson 2 is a checked IOException. JsonParseException is a separate branch from JsonMappingException, so catch JsonProcessingException to handle both.

Why the mapper must be shared: a measurement

Because introspection results are cached per mapper, a new ObjectMapper repeats the expensive work on every call. The program writes and reads a 102-character order 20,000 times, with a fresh mapper each time, a shared mapper, and a shared ObjectWriter and ObjectReader. Each variant gets one warm-up run, then the best of 3 runs is reported. This is a rough comparison on a 2-CPU Linux machine with JDK 21, not a benchmark; see the JMH page for measuring properly.

Java
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:

Output
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 Order and Item again, not into writing 102 characters.
  • The prebuilt ObjectWriter and ObjectReader 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.
  • The practical rule: create mappers at start-up and keep them in a static final field or a dependency injection bean (a Spring Boot application already has one). Use readers and writers for variations instead of new mappers.

Jackson 3

Jackson 3.0 was released in 2025 with a new package name, tools.jackson, so it can sit next to Jackson 2 in the same application. The annotations stay in com.fasterxml.jackson.annotation and are shared by both. The program repeats the earlier examples with Jackson 3.1.

Java
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):

Output
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
Defaults that changed from Jackson 2 to Jackson 3, as observed on this page
BehaviorJackson 2.21Jackson 3.1
java.time supportNeeds JavaTimeModuleBuilt in
Date formatTimestamps (arrays)ISO-8601 strings
Unknown propertiesFailIgnored
Missing or null primitiveSilently 0Fails: FAIL_ON_NULL_FOR_PRIMITIVES is on
Property orderDeclaration orderAlphabetical
ExceptionsChecked JsonProcessingExceptionUnchecked JacksonException
ConfigurationBuilder or mutable mapperBuilder only; rebuild() for variants

The missing price, which Jackson 2 silently turned into 0.0, now fails, and so does an explicit null. The bean with fields zeta and alpha was written alphabetically. Code that compares JSON as text, or relies on lenient primitives, needs attention when upgrading. Spring Boot 4 uses Jackson 3, and Spring Boot 3 uses Jackson 2. The other Jackson pages on this site use Jackson 2, whose concepts carry over to version 3.

Choosing settings and APIs

What to use for common needs
NeedUse
Application-wide mapperOne JsonMapper.builder()...build(), stored and shared
Pretty printing for one callmapper.writerWithDefaultPrettyPrinter()
Reading a generic type such as List<User>mapper.readerFor(new TypeReference<List<User>>() { })
Reading another team's APIdisable(FAIL_ON_UNKNOWN_PROPERTIES)
Dates as textJavaTimeModule plus disable(WRITE_DATES_AS_TIMESTAMPS) (built in on Jackson 3)
Detecting absent numbersWrapper types, or FAIL_ON_NULL_FOR_PRIMITIVES
Object to Map or between typesconvertValue
Partial update of an objectreaderForUpdating(target)
Unknown or changing JSON shapereadTree and the tree model
Different JSON per class without changing itMix-ins or custom serializers