In a previous blog post, I wrote about JSON-P (JSR 374), the Java API for processing JSON. JSON-P offers two models for parsing and creating JSON data: a streaming API and an object model API. While powerful, it operates at a low level, requiring you to work directly with JSON structures like objects and arrays.
Often, you need to convert Java objects directly to and from JSON. This is where the Java API for JSON Binding (JSON-B), or JSR 367, comes in. JSON-B is a standard binding layer for converting Java objects to and from JSON documents. It builds on JSON-P and provides a more convenient way to handle JSON when you have a clear Java model. JSON-B is part of Jakarta EE 10.
By default, JSON-B serializes all public fields of a class. For deserialization, it uses a public no-argument constructor to create an instance and then populates its public fields. JSON-B also works with Java records.
Beyond basic serialization and deserialization, JSON-B offers a set of features to customize the binding process. You can control property names and order, include or exclude fields, handle polymorphism, work with collections and generics, and even define custom adapters for specific types.
Setup ¶
JSR 367 is an API specification, so you need an implementation to use it. The official reference implementation is Eclipse Yasson.
To get started with JSON-B, add its API and the Yasson implementation to your project. Here are the required Maven dependencies:
<dependency>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>3.0.4</version>
</dependency>
Another implementation of the JSON-B API is Johnzon. Since your application only references the JSON-B API, you can easily switch implementations by changing the implementation dependency.
Basic Serialization and Deserialization ¶
The entry point to the JSON-B API is the Jsonb
interface. You can get an instance using the JsonbBuilder
.
Jsonb jsonb = JsonbBuilder.create();
Let's take a simple Person
record:
public record Person(
String name,
int age,
String email,
String password,
LocalDate createdDate,
List<String> hobbies,
Map<String, String> metadata) {}
Now, let's see how to serialize an instance of this record to a JSON string and deserialize it back.
Person person =
new Person("John Doe", 30, "john@example.com", "secret", LocalDate.now(), null, null);
String json = jsonb.toJson(person);
System.out.println(json);
// {"age":30,"createdDate":"2025-09-10","email":"john@example.com","name":"John Doe","password":"secret"}
Person deserializedPerson = jsonb.fromJson(json, Person.class);
System.out.println(deserializedPerson.name());
// Output: John Doe
JSON-B automatically handles the LocalDate
type by converting it to a standard ISO-8601 date string and
converting it back during deserialization. We also see that null values are omitted by default during serialization.
Customizing with Annotations ¶
JSON-B provides a set of annotations to control the binding process. Let's modify our Person
record to see some of them in action.
@JsonbPropertyOrder({"name", "email", "age", "createdDate"})
public record Person(
@JsonbProperty("full_name") String name,
int age,
String email,
@JsonbTransient String password,
LocalDate createdDate,
List<String> hobbies,
Map<String, String> metadata) {}
Here's what we did:
@JsonbProperty("full_name")
: This annotation changes the name of thename
field tofull_name
in the JSON output and vice-versa during deserialization.@JsonbTransient
: This annotation excludes thepassword
field from both serialization and deserialization. This is useful for sensitive information.@JsonbPropertyOrder
: This annotation specifies the order of properties in the resulting JSON.
Now, when we serialize the object, the output will reflect these changes:
Person person =
new Person("John Doe", 30, "john@example.com", "secret", LocalDate.now(), null, null);
String json = jsonb.toJson(person);
System.out.println(json);
// Output:
// {"full_name":"John Doe","email":"john@example.com","age":30,"createdDate":"2025-09-10"}
Custom Adapters ¶
Sometimes, you need to serialize a type in a custom format. JSON-B allows you to provide a JsonbAdapter
for this purpose. Let's say we have an Event
record with a LocalDateTime
.
public record Event(
String title,
LocalDateTime eventDate,
String description) {
Now we want to serialize the eventDate
field using DateTimeFormatter.ISO_LOCAL_DATE_TIME
and parse it from that format during deserialization.
First, we define the adapter by implementing the JsonbAdapter
interface. This interface requires two methods: adaptToJson
for serialization and adaptFromJson
for deserialization.
public static class LocalDateTimeAdapter implements JsonbAdapter<LocalDateTime, String> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
@Override
public String adaptToJson(LocalDateTime obj) throws Exception {
return obj.format(FORMATTER);
}
@Override
public LocalDateTime adaptFromJson(String obj) throws Exception {
return LocalDateTime.parse(obj, FORMATTER);
}
}
Then, we apply the adapter to the field using @JsonbTypeAdapter
:
public record Event(
String title,
@JsonbTypeAdapter(LocalDateTimeAdapter.class) LocalDateTime eventDate,
String description) {
Now, JSON-B will use our adapter for the eventDate
field.
Event event =
new Event("Tech Conference", LocalDateTime.now(), "Annual developer conference");
String eventJson = jsonb.toJson(event);
System.out.println(eventJson);
// Output:
// {"description":"Annual developer
// conference","eventDate":"2025-09-10T08:09:51.1675905","title":"Tech Conference"}
Handling Polymorphism ¶
JSON-B can also handle polymorphism. Imagine you have an abstract Animal
class with two implementations, Dog
and Cat
.
@JsonbTypeInfo({
@JsonbSubtype(alias = "dog", type = Dog.class),
@JsonbSubtype(alias = "cat", type = Cat.class)
})
public abstract class Animal {
public class Dog extends Animal {
public class Cat extends Animal {
The @JsonbTypeInfo
and @JsonbSubtype
annotations tell JSON-B to include a special property (@type
by default) in the JSON to store the object's type. This allows it to deserialize the JSON back to the correct concrete class.
List<Animal> animals =
Arrays.asList(new Dog("Buddy", "Golden Retriever"), new Cat("Whiskers", true));
String animalsJson = jsonb.toJson(animals);
System.out.println(animalsJson);
// Output:
// [{"@type":"dog","name":"Buddy","breed":"Golden
// Retriever"},{"@type":"cat","name":"Whiskers","indoor":true}]
Animal[] deserializedAnimals = jsonb.fromJson(animalsJson, Animal[].class);
for (Animal animal : deserializedAnimals) {
System.out.println(animal.getName() + " says: " + animal.makeSound());
}
// Output:
// Buddy says: Woof!
// Whiskers says: Meow!
Collections and Generics ¶
JSON-B seamlessly handles collections and maps, correctly serializing and deserializing generic types.
Consider this DataContainer
record:
public record DataContainer(List<Person> people, Set<String> tags, Map<String, Integer> scores) {}
When we serialize an instance of DataContainer
, JSON-B produces the expected JSON arrays and objects.
List<Person> people =
Arrays.asList(
new Person("Alice", 28, "alice@test.com", "pass", LocalDate.now(), null, null),
new Person("Bob", 35, "bob@test.com", "pass", LocalDate.now(), null, null));
Set<String> tags = new HashSet<>(Arrays.asList("java", "json", "tutorial"));
Map<String, Integer> scores = new HashMap<>();
scores.put("test1", 95);
scores.put("test2", 87);
DataContainer container = new DataContainer(people, tags, scores);
String containerJson = jsonb.toJson(container);
System.out.println(containerJson);
// Output:
// {"people":[{"full_name":"Alice","email":"alice@test.com","age":28,"createdDate":"2025-09-10"},
// {"full_name":"Bob","email":"bob@test.com","age":35,"createdDate":"2025-09-10"}],
// "scores":{"test2":87,"test1":95},"tags":["java","json","tutorial"]}
During deserialization, it uses the generic type information to create the correct collection types with the right content.
DataContainer deserializedContainer = jsonb.fromJson(containerJson, DataContainer.class);
System.out.println(deserializedContainer.people().size());
// Output: 2
System.out.println(deserializedContainer.people().get(0).name());
// Output: Alice
System.out.println(deserializedContainer.tags());
// Output: [java, json, tutorial]
System.out.println(deserializedContainer.scores());
// Output: {test2=87, test1=95}
Configuration ¶
For more advanced customization, you can use JsonbConfig
. It allows you to configure various aspects like formatting and null handling.
JsonbConfig config = new JsonbConfig().withNullValues(true).withFormatting(true);
Jsonb configuredJsonb = JsonbBuilder.create(config);
Person personWithNulls = new Person("Test User", 0, null, null, null, null, null);
String formattedJson = configuredJsonb.toJson(personWithNulls);
System.out.println(formattedJson);
// Output:
// {
// "full_name": "Test User",
// "email": null,
// "age": 0,
// "createdDate": null,
// "hobbies": null,
// "metadata": null
// }
The output will be a nicely formatted JSON string with all null-valued fields included.
Fine-Grained Null Handling ¶
By default, JSON-B omits fields that are null. As seen in the example above, you can reverse this behavior globally using the withNullValues(true)
configuration option, which makes the serializer include all fields, even if they are null.
What if you want to override the default behavior for specific fields? In that case, you can use the @JsonbNillable
annotation.
Depending on the default configuration, the @JsonbNillable
annotation can force a field to always be included in the JSON if it is null (@JsonbNillable(true)
)
or to always be omitted if it is null (@JsonbNillable(false)
).
public record TestNillable(@JsonbNillable String nillableField, String nonNillableField) {}
Even with a Jsonb
instance configured to omit nulls, the nillableField
will be present in the JSON with a value of null
if it is null in the object.
TestNillable testNillable1 = new TestNillable(null, null);
json = jsonb.toJson(testNillable1);
System.out.println(json);
// Output: {"nillableField":null}
Handling Binary Data ¶
JSON-B can serialize byte[]
fields into a JSON string. You can control the encoding format using the withBinaryDataStrategy
configuration option.
There are three strategies available:
BASE_64
: The default strategy. It uses standard Base64 encoding, which includes characters like+
and/
.BASE_64_URL
: This strategy uses a URL-safe Base64 encoding, replacing+
with-
and/
with_
. This is useful when the encoded string needs to be part of a URL.BYTE
: This strategy serializes thebyte[]
as a JSON array of integer values.
Let's see how they differ with an example. First, a simple record to hold binary data:
public record BinaryData(String filename, byte[] content) {}
Now, we'll serialize an instance of this record using each of the three strategies.
Strategy 1: BASE_64 ¶
byte[] testBytes = {(byte) 0x3E, (byte) 0x3F, (byte) 0xFE, (byte) 0xFF};
BinaryData binaryData = new BinaryData("test.bin", testBytes);
JsonbConfig base64Config =
new JsonbConfig()
.withBinaryDataStrategy(jakarta.json.bind.config.BinaryDataStrategy.BASE_64);
Jsonb base64Jsonb = JsonbBuilder.create(base64Config);
String base64Json = base64Jsonb.toJson(binaryData);
System.out.println(base64Json);
// Output: {"content":"Pj/+/w==","filename":"test.bin"}
Strategy 2: BASE_64_URL ¶
JsonbConfig base64UrlConfig =
new JsonbConfig()
.withBinaryDataStrategy(jakarta.json.bind.config.BinaryDataStrategy.BASE_64_URL);
Jsonb base64UrlJsonb = JsonbBuilder.create(base64UrlConfig);
String base64UrlJson = base64UrlJsonb.toJson(binaryData);
System.out.println(base64UrlJson);
// Output: {"content":"Pj_-_w==","filename":"test.bin"}
Strategy 3: BYTE ¶
JsonbConfig byteConfig =
new JsonbConfig()
.withBinaryDataStrategy(jakarta.json.bind.config.BinaryDataStrategy.BYTE);
Jsonb byteJsonb = JsonbBuilder.create(byteConfig);
String byteJson = byteJsonb.toJson(binaryData);
System.out.println(byteJson);
// Output: {"content":[62,63,-2,-1],"filename":"test.bin"}
As you can see, BASE_64
produces a string with /
and +
, while BASE_64_URL
produces a URL-safe string with _
and -
.
The BYTE
strategy is even more verbose, representing the byte array as a JSON array of integers. It is not recommended to use
JSON for large binary data, as all these strategies significantly increase the data size. Base64 encoding increases the size by about 33%,
and the BYTE
strategy is even larger, depending on the byte values.
Customizing Property Names with Strategies ¶
While you can rename individual fields with @JsonbProperty
, JSON-B also offers global naming strategies via
withPropertyNamingStrategy
. This is useful for consistently applying a naming convention across all properties of an object. JSON-B provides several built-in strategies:
IDENTITY
: This is the default. It uses the Java field name as is (e.g.,userName
remainsuserName
).LOWER_CASE_WITH_DASHES
: Converts camelCase to kebab-case (e.g.,userName
becomesuser-name
).LOWER_CASE_WITH_UNDERSCORES
: Converts camelCase to snake_case (e.g.,userName
becomesuser_name
).UPPER_CAMEL_CASE
: Converts the first character to uppercase (e.g.,userName
becomesUserName
).UPPER_CAMEL_CASE_WITH_SPACES
: Converts camelCase to Title Case with spaces (e.g.,userName
becomesUser Name
).CASE_INSENSITIVE
: This behaves the same asIDENTITY
during serialization. However, during deserialization, it ignores case differences when matching JSON properties to Java fields (e.g.,UserName
,username
, andUSERNAME
would all map touserName
).
Note that these strategies also affect how JSON-B matches JSON properties to Java fields during deserialization.
For example, with the LOWER_CASE_WITH_UNDERSCORES
strategy, a JSON property named user_name
would correctly map to a Java field named userName
.
Let's see an example with LOWER_CASE_WITH_UNDERSCORES
.
First, we have a UserProfile
record with camelCase fields:
public record UserProfile(
String userName, String displayName, String emailAddress, String jobTitle) {}
Now, we configure JSON-B with the LOWER_CASE_WITH_UNDERSCORES
strategy:
JsonbConfig namingConfig =
new JsonbConfig()
.withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES);
Jsonb namingJsonb = JsonbBuilder.create(namingConfig);
UserProfile userProfile =
new UserProfile("john.doe", "John Doe", "john@example.com", "Software Engineer");
String namingJson = namingJsonb.toJson(userProfile);
System.out.println(namingJson);
// Output:
// {"display_name":"John Doe","email_address":"john@example.com","job_title":"Software
// Engineer","user_name":"john.doe"}
The output will have all property names in snake_case.
Deserialization also works seamlessly with this strategy. When we take the JSON string with snake_case properties and
deserialize it back to a UserProfile
object, JSON-B correctly maps the properties back to the camelCase fields.
UserProfile deserializedUserProfile = namingJsonb.fromJson(namingJson, UserProfile.class);
System.out.println(deserializedUserProfile);
// Output:
// UserProfile[userName=john.doe, displayName=John Doe, emailAddress=john@example.com,
// jobTitle=Software Engineer]
Error Handling ¶
When deserializing, the input JSON might not match the target Java class, which can lead to errors. It's good practice to wrap your
fromJson
calls in a try-catch block to handle potential JsonbException
s.
String invalidJson = "{\"name\": \"John\", \"age\": \"not-a-number\"}";
try {
jsonb.fromJson(invalidJson, Person.class);
System.out.println("This shouldn't print");
} catch (JsonbException e) {
System.out.println("Caught deserialization error: " + e.getMessage());
}
// Output: Caught deserialization error: Unable to deserialize property 'age' because of:
// Error deserialize JSON value into type: int.
Conclusion ¶
JSON-B provides a simple way to map Java objects to and from JSON. It offers sensible defaults for common use cases and a set of annotations and configuration options for customization. Since JSON-B is an API specification, you need to choose an implementation like Eclipse Yasson or Apache Johnzon. You can easily switch between implementations without changing your application code because the code only references the JSON-B API.