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.
JsonMapper 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.coreand Java packages undertools.jackson. The annotations keep their old package,com.fasterxml.jackson.annotation, so annotated DTOs mostly carry over. JacksonExceptionis unchecked. In Jackson 2,readValueandwriteValueAsStringthrowJsonProcessingException, a checkedIOException.- Mappers are immutable and configured through a builder such as
JsonMapper.builder(). - Support for
java.timeis built in. Jackson 2 needs thejackson-datatype-jsr310module registered, or serialising anInstantfails. - Several defaults changed:
java.timevalues 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
ObjectMapperrejects JSON fields that the target type does not have; Jackson 3 ignores them by default (FAIL_ON_UNKNOWN_PROPERTIESis 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
nulltargets a primitive such asint; use a wrapper type when a field is optional. - Nulls. Jackson writes
nullfields 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
@JsonTypeInfoand@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
BigDecimalfields for money; a typedBigDecimaltarget keeps every digit of the JSON number.
Example
// 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(("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.
<!-- 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.
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.
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