Home | Send Feedback | Share on Bluesky |

JSON-B (JSR 367) - Java API for JSON Binding

Published: 10. September 2025  •  java, json

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>

pom.xml

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) {}

Person.java

Here's what we did:

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"}

App.java

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);
    }
  }

Event.java

Then, we apply the adapter to the field using @JsonbTypeAdapter:

public record Event(
    String title,
    @JsonbTypeAdapter(LocalDateTimeAdapter.class) LocalDateTime eventDate,
    String description) {

Event.java

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"}

App.java

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 {

Animal.java

public class Dog extends Animal {

Dog.java

public class Cat extends Animal {

Cat.java

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!

App.java

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) {}

DataContainer.java

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"]}

App.java

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}

App.java

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
      // }

App.java

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) {}

TestNillable.java

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}

App.java

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:

Let's see how they differ with an example. First, a simple record to hold binary data:

public record BinaryData(String filename, byte[] content) {}

BinaryData.java

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"}

App.java


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"}

App.java


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"}

App.java

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:

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) {}

UserProfile.java

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"}

App.java

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]

App.java

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 JsonbExceptions.

      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.

App.java

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.