Java XML and JSON: Document processing for Java SE, Part 2: JSON-B

how-to
Mar 14, 201917 mins

Bind Java objects to JSON documents with the Java API for JSON Binding

data conversion
Credit: erhui1979 / Getty Images

In this article, we’ll continue exploring XML and JSON in Java 11 and beyond.

Examples in this article will introduce you to JSON-B, the JSON Binding API for Java. After a quick overview and installation instruction, I’ll show you how to use JSON-B to serialize and deserialize Java objects, arrays, and collections; how to customize serialization and deserialization using JSON-B; and how to use JSON-B adapters to convert source objects to target objects during serialization or deserialization.

The material for this article is completely new, but could be considered an additional chapter (Chapter 13) for my new book, recently published by Apress: Java XML and JSON, Second Edition.

What is JSON-B?

JSON-B is a standard binding layer and API for converting Java objects to and from JSON documents. It’s similar to Java Architecture for XML Binding (JAXB), which is used to convert Java objects to and from XML.

JSON-B is built on top of JSON-P, the JSON Processing API used for parsing, generating, querying, and transforming JSON documents. JSON-B was introduced by Java Specification Request (JSR) 367 more than a year after the final release of JSR 353, the JSR for JSON-P.

The JSON-B API

The Java API for JSON Binding (JSON-B) website introduces JSON-B and provides access to various resources, including API documentation. According to the documentation, the JSON-B module stores six packages:

  • javax.json.bind: Defines the entry point for binding Java objects to JSON documents.
  • javax.json.bind.adapter: Defines adapter-related classes.
  • javax.json.bind.annotation: Defines annotations for customizing the mapping between Java program elements and JSON documents.
  • javax.json.bind.config: Defines strategies and policies for customizing the mapping between Java program elements and JSON documents.
  • javax.json.bind.serializer: Defines interfaces for creating custom serializers and deserializers.
  • javax.json.bind.spi: Defines a Service Provider Interface (SPI) for plugging in custom JsonbBuilders.

The JSON-B website also provides a link to Yasson, a Java framework that provides a standard binding layer between Java classes and JSON documents, and an official reference implementation of the JSON Binding API.

Download and install JSON-B

JSON-B 1.0 is the current version at the time of writing. You can obtain the Yasson reference implementation of this library from the Maven repository. You will need to download the following JAR files:

  • Javax JSON Bind API 1.0: Contains all of the JSON-B classfiles. I downloaded javax.json.bind-api-1.0.jar.
  • Yasson: Contains the Eclipse-based reference implementation of JSON-B. I downloaded yasson-1.0.3.jar.
  • JSR 374 (JSON Processing) Default Provider: Contains all of the JSON-P 1.0 classfiles along with the Glassfish default provider classfiles. I downloaded javax.json-1.1.4.jar.

Add these JAR files to your classpath when compiling and running code that uses these libraries:

javac -cp javax.json.bind-api-1.0.jar;. main source file
java -cp javax.json.bind-api-1.0.jar;yasson-1.0.3.jar;javax.json-1.1.4.jar;. main classfile

Serializing and deserializing Java objects with JSON-B

The javax.json.bind package provides the Jsonb and JsonbBuilder interfaces, which serve as the entrypoint to this library:

  • Jsonb provides overloaded toJson() methods for serializing trees of Java objects to JSON documents, and fromJson() methods for deserializing JSON documents to trees of Java objects.
  • JsonbBuilder provides newBuilder() and other methods for obtaining a new builder, and build() and create() methods for returning new Jsonb objects.

The following code example demonstrates the basic usage of the Jsonb and JsonBuilder types:

// Create a new Jsonb instance using the default JsonbBuilder implementation.
Jsonb jsonb = JsonbBuilder.create();

// Create an Employee object from a hypothetical Employee class.
Employee employee = ...

// Convert the Employee object to a JSON document stored in a string.
String jsonEmployee = jsonb.toJson(employee);

// Convert the previously-created JSON document to an Employee object.
Employee employee2 = jsonb.fromJson(jsonEmployee, Employee.class);

This example invokes Jsonb‘s String toJson(Object object) method to serialize a Java object, (Employee). This method is passed the root of the Java object tree to serialize. If null is passed, toJson() throws java.lang.NullPointerException. It throws javax.json.bind.JsonbException when an unexpected problem (such as an I/O error) occurs during serialization.

This code fragment also invokes Jsonb‘s <T> T fromJson(String str, Class<T> type) generic method, which is used for deserialization. This method is passed the string-based JSON document to deserialize and the type of the resulting Java object tree’s root object, which is returned. This method throws NullPointerException when null is passed to either parameter; it throws JsonbException when an unexpected problem occurs during deserialization.

I excerpted the code fragment from a JSONBDemo application that provides a basic demonstration of JSON-B. Listing 1 presents the source code for this demo.

Listing 1. JSONBDemo.java (version 1)

import java.time.LocalDate;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;

public class JSONBDemo
{
   public static void main(String[] args)
   {
      Jsonb jsonb = JsonbBuilder.create();
      Employee employee = new Employee("John", "Doe", 123456789, false,
                                       LocalDate.of(1980, 12, 23),
                                       LocalDate.of(2002, 8, 14));
      String jsonEmployee = jsonb.toJson(employee);
      System.out.println(jsonEmployee);
      System.out.println();
      Employee employee2 = jsonb.fromJson(jsonEmployee, Employee.class);
      System.out.println(employee2);
   }
}

main() first creates a Jsonb object followed by an Employee object. It then calls toJson() to serialize the Employee object to a JSON document that’s stored in a string. After printing this document, main() invokes fromJson() with the previous string and Employee‘s java.lang.Class object to deserialize the JSON document to another Employee object, which is subsequently printed.

Listing 2 presents Employee‘s source code.

Listing 2. Employee.java (version 1)

import java.time.LocalDate;

public class Employee
{
   private String firstName;

   private String lastName;

   private int ssn;

   private boolean isMarried;

   private LocalDate birthDate;

   private LocalDate hireDate;

   private StringBuffer sb = new StringBuffer();

   public Employee() {}

   public Employee(String firstName, String lastName, int ssn, boolean isMarried,
                   LocalDate birthDate, LocalDate hireDate)
   {
      this.firstName = firstName;
      this.lastName = lastName;
      this.ssn = ssn;
      this.isMarried = isMarried;
      this.birthDate = birthDate;
      this.hireDate = hireDate;
   }

   public String getFirstName()
   {
      return firstName;
   }

   public String getLastName()
   {
      return lastName;
   }

   public int getSSN()
   {
      return ssn;
   }

   public boolean isMarried()
   {
      return isMarried;
   }

   public LocalDate getBirthDate()
   {
      return birthDate;
   }

   public LocalDate getHireDate()
   {
      return hireDate;
   }

   public void setFirstName(String firstName)
   {
      this.firstName = firstName;
   }

   public void setLastName(String lastName)
   {
      this.lastName = lastName;
   }

   public void setSSN(int ssn)
   {
      this.ssn = ssn;
   }

   public void setIsMarried(boolean isMarried)
   {
      this.isMarried = isMarried;
   }

   public void setBirthDate(LocalDate birthDate)
   {
      this.birthDate = birthDate;
   }

   public void setHireDate(LocalDate hireDate)
   {
      this.hireDate = hireDate;
   }

   @Override
   public String toString()
   {
      sb.setLength(0);
      sb.append("First name [");
      sb.append(firstName);
      sb.append("], Last name [");
      sb.append(lastName);
      sb.append("], SSN [");
      sb.append(ssn);
      sb.append("], Married [");
      sb.append(isMarried);
      sb.append("], Birthdate [");
      sb.append(birthDate);
      sb.append("], Hiredate [");
      sb.append(hireDate);
      sb.append("]");
      return sb.toString();
   }
}

Compile Listings 1 and 2 as follows:

javac -cp javax.json.bind-api-1.0.jar;. JSONBDemo.java

Run the application as follows:

java -cp javax.json.bind-api-1.0.jar;yasson-1.0.3.jar;javax.json-1.1.4.jar;. JSONBDemo

You should observe the following output (spread across multiple lines for readability):

{"SSN":123456789,"birthDate":"1980-12-23","firstName":"John","hireDate":"2002-08-14",
 "lastName":"Doe","married":false}

First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14] 

Rules for working with JSON-B

While playing with this application, I observed some interesting behaviors that led me to formulate the following rules concerning Employee:

  • The class must be public; otherwise, an exception is thrown.
  • toJson() will not serialize fields with non-public getter methods.
  • fromJson() will not deserialize fields with non-public setter methods.
  • fromJson() throws JsonbException in the absence of a public noargument constructor.

In order to seamlessly convert between Java object fields and JSON data, JSON-B has to support various Java types. For example, JSON-B supports the following basic Java types:

  • java.lang.Boolean
  • java.lang.Byte
  • java.lang.Character
  • java.lang.Double
  • java.lang.Float
  • java.lang.Integer
  • java.lang.Long
  • java.lang.Short
  • java.lang.String

Additional types such as java.math.BigInteger, java.util.Date, and java.time.LocalDate are supported. Check out the JSON-B specification for a complete list of supported types.

Serializing and deserializing arrays and collections with JSON-B

The previous section focused on serializing and deserializing single Java objects. JSON-B also supports the ability to serialize and deserialize object arrays and collections. Listing 3 provides a demonstration.

Listing 3. JSONBDemo.java (version 2)

import java.time.LocalDate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;

public class JSONBDemo
{
   public static void main(String[] args)
   {
      arrayDemo();
      listDemo();
   }

   // Serialize and deserialize an array of Employee objects.
   static void arrayDemo()
   {
      Jsonb jsonb = JsonbBuilder.create();
      Employee[] employees =
      {
         new Employee("John", "Doe", 123456789, false,
                      LocalDate.of(1980, 12, 23),
                      LocalDate.of(2002, 8, 14)),
         new Employee("Jane", "Smith", 987654321, true,
                      LocalDate.of(1982, 6, 13),
                      LocalDate.of(2001, 2, 9))
      };
      String jsonEmployees = jsonb.toJson(employees);
      System.out.println(jsonEmployees);
      System.out.println();
      employees = null;
      employees = jsonb.fromJson(jsonEmployees, Employee[].class);
      for (Employee employee: employees)
      {
         System.out.println(employee);
         System.out.println();
      }
   }

   // Serialize and deserialize a List of Employee objects.
   static void listDemo()
   {
      Jsonb jsonb = JsonbBuilder.create();
      List<Employee> employees =
         Arrays.asList(new Employee("John", "Doe", 123456789, false,
                                    LocalDate.of(1980, 12, 23),
                                    LocalDate.of(2002, 8, 14)),
                       new Employee("Jane", "Smith", 987654321, true,
                                    LocalDate.of(1982, 6, 13),
                                    LocalDate.of(1999, 7, 20)));
      String jsonEmployees = jsonb.toJson(employees);
      System.out.println(jsonEmployees);
      System.out.println();
      employees = null;
      employees = jsonb.fromJson(jsonEmployees,
                                 new ArrayList<>(){}.
                                     getClass().getGenericSuperclass());
      System.out.println(employees);
   }
}

Listing 3 is a simple extension of Listing 1, and uses the same Employee class presented in Listing 2. Additionally, this code example calls the same toJson() and fromJson() methods.

When deserializing a JSON document to an array of Java objects, an expression such as Employee[].class is passed as the second argument to fromJson() so that it can create the appropriate array. When deserializing a JSON object to a list or other collection, an expression such as new ArrayList<>(){}.getClass().getGenericSuperclass() is passed as the second argument. JDK 11 infers Employee so I don’t have to specify ArrayList<Employee>.

Ideally, it should be possible to pass ArrayList<Employee>.class, to tell fromJson() the expected parameterized type of the collection to instantiate. However, because of type erasure, this expression is illegal. Instead, I could specify ArrayList.class, which would work. However, it would also generate an unchecked warning message. The more complicated expression works and doesn’t result in the warning. Essentially, it instantiates an anonymous subclass of ArrayList<Employee>, obtains its Class object, and uses the Class object to obtain the parameterized type of its superclass, which happens to be ArrayList<Employee>. This parameterized type is made available to fromJson().

Compile Listings 3 and 2, and run the resulting application. You should observe the following output (spread across multiple lines for readability):

[{"SSN":123456789,"birthDate":"1980-12-23","firstName":"John","hireDate":"2002-08-14",
  "lastName":"Doe","married":false},
 {"SSN":987654321,"birthDate":"1982-06-13","firstName":"Jane","hireDate":"2001-02-09",
  "lastName":"Smith","married":true}]

First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14]

First name [Jane], Last name [Smith], SSN [987654321], Married [false],
 Birthdate [1982-06-13], Hiredate [2001-02-09]

[{"SSN":123456789,"birthDate":"1980-12-23","firstName":"John","hireDate":"2002-08-14",
  "lastName":"Doe","married":false},
 {"SSN":987654321,"birthDate":"1982-06-13","firstName":"Jane","hireDate":"1999-07-20",
  "lastName":"Smith","married":true}]

[{firstName=John, lastName=Doe, hireDate=2002-08-14, birthDate=1980-12-23, married=false,
  SSN=123456789},
 {firstName=Jane, lastName=Smith, hireDate=1999-07-20, birthDate=1982-06-13, married=true,
  SSN=987654321}]

Customizing serialization and deserialization in JSON-B

Although JSON-B does a lot for you by supporting various Java types, you might need to customize its behavior; for example, to change the output order of serialized properties. JSON-B supports compile-time and runtime customization.

Compile-time customization

JSON-B supports compile-time customization via the various annotation types that are located in its javax.json.bind.annotation package. For example, you could use JsonbDateFormat to provide a custom date format and JsonbProperty to change a field’s name. Both of these annotation types are illustrated in Listing 4’s Employee class.

Listing 4. Employee.java (version 2)

import java.time.LocalDate;

import javax.json.bind.annotation.JsonbDateFormat;
import javax.json.bind.annotation.JsonbProperty;

public class Employee
{
   @JsonbProperty("first-name")
   private String firstName;

   @JsonbProperty("last-name")
   private String lastName;

   private int ssn;

   private boolean isMarried;

   @JsonbDateFormat("MM-dd-yyyy")
   private LocalDate birthDate;

   @JsonbDateFormat("MM-dd-yyyy")
   private LocalDate hireDate;

   private StringBuffer sb = new StringBuffer();

   public Employee() {}

   public Employee(String firstName, String lastName, int ssn, boolean isMarried,
                   LocalDate birthDate, LocalDate hireDate)
   {
      this.firstName = firstName;
      this.lastName = lastName;
      this.ssn = ssn;
      this.isMarried = isMarried;
      this.birthDate = birthDate;
      this.hireDate = hireDate;
   }

   public String getFirstName()
   {
      return firstName;
   }

   public String getLastName()
   {
      return lastName;
   }

   public int getSSN()
   {
      return ssn;
   }

   public boolean isMarried()
   {
      return isMarried;
   }

   public LocalDate getBirthDate()
   {
      return birthDate;
   }

   public LocalDate getHireDate()
   {
      return hireDate;
   }

   public void setFirstName(String firstName)
   {
      this.firstName = firstName;
   }

   public void setLastName(String lastName)
   {
      this.lastName = lastName;
   }

   public void setSSN(int ssn)
   {
      this.ssn = ssn;
   }

   public void setIsMarried(boolean isMarried)
   {
      this.isMarried = isMarried;
   }

   public void setBirthDate(LocalDate birthDate)
   {
      this.birthDate = birthDate;
   }

   public void setHireDate(LocalDate hireDate)
   {
      this.hireDate = hireDate;
   }

   @Override
   public String toString()
   {
      sb.setLength(0);
      sb.append("First name [");
      sb.append(firstName);
      sb.append("], Last name [");
      sb.append(lastName);
      sb.append("], SSN [");
      sb.append(ssn);
      sb.append("], Married [");
      sb.append(isMarried);
      sb.append("], Birthdate [");
      sb.append(birthDate);
      sb.append("], Hiredate [");
      sb.append(hireDate);
      sb.append("]");
      return sb.toString();
   }
}

Listing 4 uses JsonbProperty to annotate the firstName and lastName fields, and uses JsonbDateFormat to annotate the birthDate and hireDate fields. JsonbProperty causes firstName to be serialized as first-name and lastName to be serialized as last-name. This annotation type also causes first-name to be deserialized to firstName and last-name to be deserialized to lastName. JsonbDateFormat causes the birth and hire dates to be serialized in month-day-year as opposed to the default year-month-day order, and causes JSON-B to take into account the serialized month-day-year order when deserializing.

Compile Listings 1 and 4, and run the resulting application. You should observe the following output (spread across multiple lines for readability):

{"SSN":123456789,"birthDate":"12-23-1980","first-name":"John","hireDate":"08-14-2002",
 "last-name":"Doe","married":false}

First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14]

Runtime customization

JSON-B supports runtime customization via the javax.json.bind.JsonbConfig class and JsonbBuilder. You instantiate JsonbConfig, invoke various with-prefixed methods (e.g., withPropertyOrderStrategy) to configure this object, and make the configured JsonbConfig object available to JsonBuilder, possibly by passing it as an argument to JsonbBuilder‘s static Jsonb create(JsonbConfig config) method. Check out Listing 5.

Listing 5. JSONBDemo.java (version 3)

import java.time.LocalDate;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;

import static javax.json.bind.config.PropertyOrderStrategy.*;

public class JSONBDemo
{
   public static void main(String[] args)
   {
      JsonbConfig config = new JsonbConfig()
                               .withPropertyOrderStrategy(REVERSE);
      Jsonb jsonb = JsonbBuilder.create(config);
      Employee employee = new Employee("John", "Doe", 123456789, false,
                                       LocalDate.of(1980, 12, 23),
                                       LocalDate.of(2002, 8, 14));
      String jsonEmployee = jsonb.toJson(employee);
      System.out.println(jsonEmployee);
      System.out.println();
      Employee employee2 = jsonb.fromJson(jsonEmployee, Employee.class);
      System.out.println(employee2);
   }
}

Listing 5’s main() method first instantiates JsonbConfig and then invokes this class’s JsonbConfig withPropertyOrderStrategy(String propertyOrderStrategy) method to change the property order strategy to javax.json.bind.config.PropertyOrderStrategy.REVERSE. This strategy order causes properties to be output in the reverse order to how they are normally output.

The JsonbConfig object is passed to create(JsonbConfig) to configure the resulting Jsonb object that JsonbBuilder ultimately returns. The rest of the method is the same as that shown in Listing 1.

Compile Listings 2 and 5, and run the resulting application. You should observe the following output (spread across multiple lines for readability):

{"married":false,"lastName":"Doe","hireDate":"2002-08-14","firstName":"John",
 "birthDate":"1980-12-23","SSN":123456789}

First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14]

You can accomplish this same reverse-property-order task by using one of JSON-B’s annotation types. I’ll leave figuring out how to do this as an exercise.

Using adapters in JSON-B

Finally, JSON-B supports adapters, which are objects that convert source objects to target objects during serialization or deserialization. For example, you might use an adapter to encrypt an object’s field names and values in a JSON document.

An adapter consists of the original Java object, the adapted/transformed object containing modified/additional fields, and the adapter object, which is an instance of the javax.json.bind.adapter.Adapter<Original,Adapted> type.

The Adapter type provides the following methods:

  • Original adaptFromJson(Adapted obj): This method is called during deserialization to convert Adapted to Original.
  • Adapted adaptToJson(Original obj): This method is called during serialization to convert Original to Adapted, which is then serialized to JSON.

Either method is declared with a throws Exception clause to indicate that it can throw any kind of exception during conversion.

Listing 6 presents the source code to IdentityAdapter, an adapter that doesn’t change anything. However, it prints out the objects that would otherwise be adapted, and it demonstrates adapter architecture.

Listing 6. IdentityAdapter.java

import javax.json.bind.adapter.JsonbAdapter;

public class IdentityAdapter implements JsonbAdapter<Employee, Employee>
{
   @Override
   public Employee adaptFromJson(Employee obj)
   {
      System.out.println("Deserializing: " + obj);
      return obj;
   }

   @Override
   public Employee adaptToJson(Employee obj)
   {
      System.out.println("Serializing: " + obj);
      return obj;
   }
}

You work with JsonbConfig and its JsonbConfig withAdapters(JsonbAdapter...) method to register one or more adapters:

JsonbConfig config = new JsonbConfig()
                         .withAdapters(new IdentityAdapter());

You then pass this object to JsonbBuilder‘s create(JsonbConfig) method, as I previously showed. For completeness, Listing 7’s JSONBDemo source code demonstrates both tasks.

Listing 7. JSONBDemo.java (version 4)

import java.time.LocalDate;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;

public class JSONBDemo
{
   public static void main(String[] args)
   {
      JsonbConfig config = new JsonbConfig()
                               .withAdapters(new IdentityAdapter());
      Jsonb jsonb = JsonbBuilder.create(config);
      Employee employee = new Employee("John", "Doe", 123456789, false,
                                       LocalDate.of(1980, 12, 23),
                                       LocalDate.of(2002, 8, 14));
      String jsonEmployee = jsonb.toJson(employee);
      System.out.println(jsonEmployee);
      System.out.println();
      Employee employee2 = jsonb.fromJson(jsonEmployee, Employee.class);
      System.out.println(employee2);
   }
}

Compile Listings 2, 6, and 7, and run the resulting application. You should observe the following output (spread across multiple lines for readability):

Serializing: First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14]
{"SSN":123456789,"birthDate":"1980-12-23","firstName":"John","hireDate":"2002-08-14",
 "lastName":"Doe","married":false}

Deserializing: First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14]
First name [John], Last name [Doe], SSN [123456789], Married [false],
 Birthdate [1980-12-23], Hiredate [2002-08-14]

Conclusion

JSON-B nicely complements JSON-P, which I cover in Chapter 12 of my book, Java XML and JSON, Second Edition. In this post I’ve introduced JSON-B and showed you how to use it to serialize and deserialize Java objects, arrays, and collections. I’ve also showed you how to customize serialization and deserialization using JSON-B, and introduced you to JSON-B adapters, which can be used to convert source objects to target objects during serialization or deserialization.

I’m sure that JSON-B will continue to evolve, and could be a great addition to the third edition of my book. Meanwhile, I recommend learning more about JSON-B by exploring the various methods and annotation types not covered in this post.