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

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

1 2 Page 2
Page 2 of 2

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.

1 2 Page 2
Page 2 of 2