JSON Libraries: Jackson and JSON-B

Map JSON to records and back with Jackson or JSON-B, know what changed in Jackson 3, and configure a mapper safely for a web service.

SyntaxJsonMapper mapper = JsonMapper.builder().build(); // Jackson 3 Order o = mapper.readValue(json, Order.class); String out = mapper.writeValueAsString(o); Jsonb jb = JsonbBuilder.create(); // JSON-B Order o2 = jb.fromJson(json, Order.class);

Once request bodies need to become objects, use a library. Two APIs cover most Java services: Jackson, the default in Spring Boot and the most widely used, and JSON-B (Jakarta JSON Binding), the Jakarta EE standard with Eclipse Yasson as its reference implementation. Both map JSON objects to Java types by property name and back again.

Jackson 2 and Jackson 3

Jackson 3.0 was released in October 2025 and Spring Boot 4 builds on it. The changes you will notice:

  • New Maven group tools.jackson.core and Java packages under tools.jackson. The annotations keep their old package, com.fasterxml.jackson.annotation, so annotated DTOs mostly carry over.
  • JacksonException is unchecked. In Jackson 2, readValue and writeValueAsString throw JsonProcessingException, a checked IOException.
  • Mappers are immutable and configured through a builder such as JsonMapper.builder().
  • Support for java.time is built in. Jackson 2 needs the jackson-datatype-jsr310 module registered, or serialising an Instant fails.
  • Several defaults changed: java.time values are written as ISO-8601 strings ("2026-10-13T10:00:00Z") instead of numeric timestamps, unknown JSON properties are ignored instead of rejected, and class properties are sorted alphabetically (records keep their component order). Compare a few real responses before and after the upgrade.
  • Java 17 is the minimum version.

Many running services are still on Jackson 2 through Spring Boot 3, so you will see both for a while.

Records just work

Jackson (since 2.12) reads records through their canonical constructor, so the compact constructor validation from the DTO lesson runs during deserialisation. Rename a JSON property with @JsonProperty on the component. JSON-B uses @JsonbProperty for the same job and @JsonbTransient to leave a field out.

Defaults that differ

  • Unknown properties. Jackson 2's plain ObjectMapper rejects JSON fields that the target type does not have; Jackson 3 ignores them by default (FAIL_ON_UNKNOWN_PROPERTIES is off). Ignoring lets older clients keep working when the API gains fields, but also hides typos in request bodies, so turn the check back on where strictness matters.
  • Null for primitives. Jackson 3 fails when JSON null targets a primitive such as int; use a wrapper type when a field is optional.
  • Nulls. Jackson writes null fields by default; JSON-B leaves them out.
  • Property order. JSON-B sorts properties alphabetically by default. Both Jackson lines write record components in declaration order. For ordinary classes, Jackson 2 usually follows field declaration order while Jackson 3 sorts properties alphabetically by default. Clients should not depend on property order either way.

Using a mapper well

  • Create the mapper once and share it. It is thread-safe after configuration and expensive to build, so one per request wastes CPU.
  • Deserialise into DTO records, not entities, so clients cannot set fields they should not.
  • Never turn on Jackson's default typing for untrusted input. Letting the JSON choose which class to instantiate has caused a long list of remote code execution vulnerabilities. For polymorphic types, list the allowed subtypes explicitly with @JsonTypeInfo and @JsonSubTypes.
  • Map parse errors to a 400 response with a short message, and do not echo the raw exception text, which can include internal class names.
  • Use BigDecimal fields for money; a typed BigDecimal target keeps every digit of the JSON number.

Example

Example · java
// Jackson 3: needs tools.jackson.core:jackson-databind on the classpath.
import com.fasterxml.jackson.annotation.JsonProperty;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.json.JsonMapper;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;

record Item(String sku, int quantity) {}

record CreateOrderRequest(@JsonProperty("customer_id") String customerId, List<Item> items) {
    CreateOrderRequest {
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("items must not be empty");
        }
        items = List.copyOf(items);
    }
}

record OrderResponse(String id, String status, BigDecimal total, Instant createdAt) {}

public class OrderJson {
    // Thread-safe and costly to build: create once, share everywhere.
    private static final JsonMapper MAPPER = JsonMapper.builder().build();

    public static void main(String[] args) {
        String body = """
            {"customer_id": "c-7", "items": [{"sku": "SKU-1", "quantity": 2}]}
            """;
        try {
            CreateOrderRequest request = MAPPER.readValue(body, CreateOrderRequest.class);
            System.out.println(request.customerId() + " ordered " + request.items().size() + " item(s)");
        } catch (JacksonException e) {
            // Includes failures thrown from the compact constructor.
            System.out.println("400 Bad Request");
        }

        var response = new OrderResponse("ord_1001", "PENDING", new BigDecimal("19.90"),
                Instant.parse("2026-10-13T09:30:00Z"));
        System.out.println(MAPPER.writeValueAsString(response));
    }
}

When to use it

  • Bind a JSON order request to validated records in a REST endpoint and return 400 when the body does not parse.
  • Upgrade a service from Spring Boot 3 to 4 and move its custom mapper configuration from Jackson 2 to Jackson 3.
  • Write JSON in a Jakarta EE application with the server's JSON-B implementation, keeping password hashes out with @JsonbTransient.

More examples

Adding the dependencies with Maven

jackson-databind pulls in jackson-core and jackson-annotations, so one dependency is enough. Prefer the newest patch release of each, and let a framework BOM pick versions when you use one.

Example · xml
<!-- Jackson 3. With Spring Boot 4, leave out the version: Boot manages it. -->
<dependency>
    <groupId>tools.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>3.1.5</version>
</dependency>

<!-- JSON-B: the Jakarta API plus an implementation (Eclipse Yasson).
     Jakarta EE servers already provide both. -->
<dependency>
    <groupId>jakarta.json.bind</groupId>
    <artifactId>jakarta.json.bind-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.eclipse</groupId>
    <artifactId>yasson</artifactId>
    <version>3.0.0</version>
</dependency>

The same mapping with JSON-B

JSON-B code depends only on the jakarta.json.bind API, so the implementation can be swapped by the server. @JsonbTransient is the JSON-B way to keep a sensitive field out of every response.

Example · java
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbTransient;

public class Customer {
    @JsonbProperty("customer_id")
    public String id;

    public String email;

    @JsonbTransient
    public String passwordHash;   // never written to or read from JSON
}

// In another file:
public final class CustomerJson {
    // Building a Jsonb instance is expensive; create one and reuse it.
    private static final Jsonb JSONB = JsonbBuilder.create();

    public static Customer parse(String body) {
        return JSONB.fromJson(body, Customer.class);   // JsonbException (unchecked) on bad input
    }

    public static String write(Customer customer) {
        return JSONB.toJson(customer);   // null fields are left out by default
    }
}

An explicitly configured Jackson 3 mapper

A Jackson 3 mapper is immutable, so every setting goes through the builder and the result can be shared safely. In Jackson 2 the same mapper also needed the jackson-datatype-jsr310 module and threw the checked JsonProcessingException.

Example · java
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.json.JsonMapper;

// Jackson 3: tools.jackson.core:jackson-databind. java.time support is built in.
public final class Json {
    public static final JsonMapper MAPPER = JsonMapper.builder()
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)   // reject typos in request bodies
            .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)       // keep field order, as Jackson 2 did
            .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)          // already the default: ISO-8601 strings
            .build();

    private Json() {
    }
}

// MAPPER.readValue(body, OrderRequest.class) throws JacksonException,
// which is unchecked, so callers catch it only where they map it to a 400.

Discussion

  • Be the first to comment on this lesson.