Contents


Get started with the JSON Binding API, Part 2

Default mapping with JSON-B

Everyday serialization and deserialization with the JSON Binding API

Comments

Content series:

This content is part # of 4 in the series: Get started with the JSON Binding API, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Get started with the JSON Binding API, Part 2

Stay tuned for additional content in this series.

The first article in this series presented an overview of the Java API for JSON Binding 1.0 (JSON-B), including custom and default binding options. In this article you'll be introduced to JSON-B's default mappings and automatic behaviors for serializing and deserializing most Java types, including special data types.

Default mappings and internals

The JSON-B API has adopted a familiar approach to mapping fields and properties, which should be easy to pickup if you've used a JSON binder before. JSON-B's mappings apply a set of rules without requiring annotations or custom configurations. These are available out of the box and allow the developer to get up and running without any knowledge of the JSON document’s structure, or even of the JSON data interchange format itself.

The JSON-B API provides two entry-point interfaces: javax.json.bind.Jsonb and javax.json.bind.JsonBuilder. The Jsonb interface provides serialization and deserialization functionality via the overridden methods toJson() and fromJson(). The JsonbBuilder interface provides the client access point for Jsonb instances, using a set of optional configurations to build them.

It is also important to note that JSON-B is heavily dependent on functionality provided by the JSON Processing API. Drawing on JSON-P's streaming model, javax.json.stream.JsonGenerator provides the functionality for writing JSON data to an output source (via the toJson() method), while javax.json.stream.JsonParser provides forward read-only access to JSON data (via the fromJson() method).

Installing Yasson

To get started with JSON-B you need its reference implementation, Eclipse Yasson, which is available from the Maven central repository. As shown below, you will need to add the JSON Processing API to your Maven POM dependency list for JSON-B.

Listing 1. Maven coordinates
<dependency>
   <groupId>javax.json</groupId>
   <artifactId>javax.json-api</artifactId>
   <version>1.1</version>
</dependency>


<!-- JSON-P 1.1 RI -->
<dependency>
   <groupId>org.glassfish</groupId>
   <artifactId>javax.json</artifactId>
   <version>${javax.json.version}</version>
</dependency>


<!-- JSON-B 1.0 API -->
 <dependency>
     <groupId>javax.json.bind</groupId>
     <artifactId>javax.json.bind-api</artifactId>
     <version>1.0</version>
 </dependency>


<!-- JSON-B 1.0 RI -->
<dependency>
   <groupId>org.eclipse</groupId>
   <artifactId>yasson</artifactId>
   <version>1.0</version>
</dependency>

Simplest example

Let’s jump in with a simple example that does a roundtrip conversion of an instance of the Book class shown in Listing 2.

Listing 2. Book object
public class Book {
   public String title;
}

To start a serialization or deserialization you need an instance of Jsonb. You create this by calling the static factory method create() of the JsonBuilder interface. With this instance you can perform serialization and deserialization operations by selecting the appropriate overloaded toJson() or fromJson() method.

In Listing 3, I've called the simplest toJson() method and passed it a Book object.

Listing 3. Serialize the Book instance
Book book = new Book("Fun with Java"); // Sets the title field
String bookJson = JsonbBuilder.create().toJson(book);

The value of this method is a String that holds the JSON data representation of the object passed to toJson(), as shown in Listing 4.

Listing 4. JSON representation of the Book instance
{
  "title": "Fun with Java"
}

Now let’s turn our attention to the deserialization operation. It is just as simple as serialization, and also requires an instance of Jsonb. In Listing 5, I call the simplest fromJson() and pass it the JSON data to deserialize, along with its target type.

Listing 5. Deserialize the JSON representation of Book
String json = "{\"title\":\"Fun with Java\"}";
Book book = JsonbBuilder.create().fromJson(json, Book.class);

In these examples, I've used toJson() and fromJson(), two of the simplest overloaded methods available on Jsonb. Next you'll see some of the other, more complex method available from this interface.

Overloaded methods

The Jsonb interface provides overloaded toJson() and fromJson() methods that perform JSON-B's serialization and deserialization functionality. Table 1 is an overview of these 12 methods and their signatures.

Table 1. Jsonb interface methods
Modifier and typeMethod
<T> TfromJson(InputStream stream, Class<T> type)
<T> TfromJson(InputStream stream, Type runtimeType)
<T> TfromJson(Reader reader, Class<T> type)
<T> TfromJson(Reader reader, Type runtimeType)
<T> TfromJson(String string, Class<T> type)
<T> TfromJson(String string, Type runtimeType)
StringtoJson(Object object)
voidtoJson(Object object, OutputStream stream)
voidtoJson(Object object, Writer writer)
StringtoJson(Object object, Type runtimeType)
voidtoJson(Object object, Type runtimeType, OutputStream stream)
voidtoJson(Object object, Type runtimeType, Writer writer)

As you can see, the range of methods is extensive enough to support most use-case scenarios. One of the more useful solutions is the capability to connect to an InputStream or OutputStream instance. The method either accepts the Stream instance and outputs a Java™ object or writes to the Stream, depending on whether it's a serialization or deserialization operation.

Together, InputStream and OutputStream open up many creative possibilities for streaming JSON data. As an example, consider a flat file containing JSON data and performing a roundtrip operation. In Listing 6, the toJson(Object object, OutputStream stream) method is called to serialize an instance of the Book class and output to the text file: book.json.

Listing 6. Send JSON serialization to a text file OutputStream
Jsonb jsonb = JsonbBuilder.create();
jsonb.toJson(book, new FileOutputStream("book.json"));

In Listing 7, the deserialization operation is performed by calling the fromJson(InputStream stream, Class<T> type) method.

Listing 7. Deserialize JSON data stored in a text file
Book book = jsonb.fromJson(new FileInputStream("book.json"), Book.class);

Defaults for basic Java types

JSON-B applies simple rules to the serialization and deserialization of basic Java types. Basic types in Java include String, Boolean, and all Number types such as Character and AtomicInteger and their corresponding primitive types.

For primitives and their wrapper equivalence, JSON-B uses the toString() method to generate a String that is converted to a JSON Number. For some Number types (such as java.util.concurrent.atomic.LongAdder) calling the toString() method does not generate an appropriate String equivalent. In these cases, you may call the doubleValue() method to generate a double primitive.

Exception handling for Number types

The JSON-B specification requires that you instantiate a BigDecimal for all Number types except wrapper types. During deserialization, the deserialization operation creates an instance of BigDecimal by passing the JSON number as a String to its constructor, like so: new BigDecimal("10"). This becomes an issue if the target type is not BigDecimal but one of the many other Number types such as AtomicInteger.

For most basic types, the deserialization operation automatically calls the appropriate parse$Type method—as in Integer.parseInt("10") or Boolean.parseBoolean("true"). Number types that are not wrapper types may not have the parse$Type method, however, so when the deserializer attempts to call a parse$Type method, an exception will be thrown.

To see how this works, start with the simplified Book class in Listing 8.

Listing 8. SimpleBook class with AtomicInteger
public class SimpleBook {
   private AtomicInteger bookVersion;
   // Getters and setters omitted for brevity
}

The JSON data contains a single number of type Integer, as shown in Listing 9.

Listing 9. A JSON representation of SimpleBook
{
    "bookVersion":10
}

When JSON-B attempts to transform the JSON representation of SimpleBook back to the SimpleBook class, the deserialization operation identifies the target field by its name, bookVersion. It determines that bookVersion is a Number type that does not fit into the group of wrapper types, and creates an instance of BigDecimal.

The BigDecimal type is incompatible with the AtomicInteger type. As a result, the deserialization operation will throw an IllegalArgumentException, with the message "argument type mismatch." The code in Listing 10 provokes this exception:

Listing 10. Throws IllegalArgumentException
Jsonb jsonb = JsonbBuilder.create();
jsonb.fromJson(
       jsonb.toJson(new SimpleBook(new AtomicInteger(10))),
       SimpleBook.class);

In summary, roundtrip equivalence is preserved for primitives and their wrapper equivalents, and also for BigDecimal and BigInteger. Other Number types do not support roundtrip equivalence out of the box. You can resolve this situation by using JSON-B adapters, which I introduce in Part 3.

Null values in JSON-B

Note that the result of serializing a field with a null value is that the property will be excluded from the output JSON document. On deserialization, the absent property will not cause the target property to be set to null, and the setter method will not be called (or a public field set). The property's value will remain unchanged, allowing for default values to be set in the target class.

Defaults for standard Java types

JSON-B supports standard Java SE types including BigDecimal, BigInteger, URL, URI, Optional, OptionalInt, OptionalLong, and OptionalDouble.

As I mentioned previously, Number types are serialized by calling the toString() method and deserialized by using the appropriate constructor or factory method. The serialization behavior of URL and URI types is done in the same way, by calling toString(). Deserialization is performed by using the appropriate constructor or static factory method.

Optional values are serialized by retrieving their internal instance and converting it to JSON in the appropriate way for that type (typically by calling toString()). Listing 11 shows that an Optional of an Integer will serialize by calling the toString() value of that Integer.

Listing 11. Serializing an Optional<Integer> instance
public class OptionalExample {
   public Optional<Integer> value;
   // Constructors, getters and setters omitted for brevity
}


JsonBuilder.create().toJson(new OptionalExample(10))

The output JSON number is shown in Listing 12.

Listing 12. JSON representation of OptionalExample
{
  "value": 10
}

A JSON value is deserialized into the appropriate Optional<T> or OptionalInt/Long/Double value depending on the type of the target field.

Listing 13 shows an example of how a JSON number is deserialized to an OptionalInt instance.

Listing 13. Deserialization of JSON number to OptionalInt
JsonBuilder.create().fromJson("{\"value\":10}", OptionalIntExample.class)

The JSON data is deserialized to an instance of the OptionalIntExample class (Listing 14), which has a single OptionalInt field called value. After deserialization, the value field has the value OptionalInt.of(10).

Listing 14. The OptionalIntExample class
public class OptionalIntExample {
   public OptionalInt value;
   // Constructors, getters and setters omitted for brevity
}

As you can see, Optional classes are fully supported for both serialization and deserialization.

Serialization of an empty Optional is treated in the same way that nulls are treated when serializing basic types. This mean that by default they are not preserved. On deserialization, null values are represented as Optional.empty() (or Optional/Int/Long/Double.empty()) instances. Conversely, an empty Optional stored in array and map data structures are serialized as nulls in the output JSON array.

Defaults for special data types

Let's now take a look at the defaults for special data types. (In Part 3, you'll learn how to use the @JsonbNillable mapping annotation to customize the treatment of nulls.)

Enums

The serialization of an enum value calls the name() method to return a String representation of the enum constant. This is especially important because the deserialization operation calls the valueOf() method and passes it the property value. Roundtrip equivalence is respected for enums.

Date types

The JSON Binding API supports dates and time instances from both the old Date classes in the java.util package (introduced in JDK 1.1) and the new Java 8 date and time classes found in the java.time packages.

The default time zone applied is the GMT standard time zone and the offset is specified as UTC Greenwich. The date-time format is ISO 8601 without an offset. These defaults can be overridden with customization options introduced in Part 3.

Time zones instances are fully supported other than the deprecated three-letter time zone IDs. Listings 15 and 16 show two examples of how dates and times are serialized.

Listing 15. Serialize a traditional Date instance
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
Date date = sdf.parse("25/12/2018");
String json = jsonb.toJson(date);


Serialize to JSON: "2018-12-25T00:00:00Z[UTC]"
Listing 16. Serialize a Java 8 Duration instance
Duration.ofHours(4).plusMinutes(3).plusSeconds(2)


Serialize to JSON: "PT4H3M2S"

See the JSON-B specification documentation (section 3.5) for the lowdown on what format is used with each type.

Arrays and collection types

All types of collections and arrays are supported and serialize to JSON arrays, while Map types serialize to JSON maps. The serialization of a Map is shown in Listing 17, and its JSON representation is shown in Listing 18.

Listing 17. A map of Strings and Integers
new HashMap<String, Integer>() {{
   put("John", 10);
   put("Jane", 10);
}};
Listing 18. JSON representation of a Map
{
  "John": 10,
  "Jane": 10
}

When a Collection or Map is a member of a class, its field name is used as the property name for the resulting JSON object, as shown in Listing 19 and Listing 20.

Listing 19. A Map as a member of a class
public class ScoreBoard {
   public Map<String, Integer> scores = 
new HashMap<String, Integer>() {{
       put("John", 12);
       put("Jane", 34);
   }};


}
Listing 20. The JSON representation
{
 "scores": {
   "John": 12,
   "Jane": 34
 }
}

JSON-B also supports multidimensional primitive and Java type arrays.

Null values in collections and maps

JSON-B's treatment of null values within array and map structures bucks convention by preserving nulls on both serialization and deserialization. This means that empty elements are serialized to nulls and vice versa. Empty Optional values are serialized as nulls in the output JSON array and deserialized as Optional.empty() instances.

Untyped mapping

Dates, times, and calendar type are numerous, including both the old java.util.Date and java.util.Calendar types and the new Java 8 date and time classes found in java.time.

As we have seen, JSON property data is deserialized to the Java type of the corresponding field in the target class; however, if the output type is unspecified or is specified as Object, the JSON value will be deserialized to the corresponding default Java type, as shown in Table 2.

Table 2. Default deserialization types
JSON valueJava type
objectjava.util.Map<String, Object>
arrayjava.util.List<Object>
stringjava.lang.String
numberjava.math.BigDecimal
true, falsejava.lang.Boolean
nullnull

Listing 21 shows what this means in practice, where a JSON object is deserialized to a Java Map instance.

Listing 21. A JSON object deserialized to a Java Map
String json = "{
\"title\":\"Fun with Java\",
\"price\":24.99,
\"issue\":null}";
Map<String, Object>() map = 
JsonbBuilder.create().fromJson(json, Map.class);

Java class serialization and deserialization

The deserialize operation expects the target class to have a no-argument public or protected constructor, or to explicitly specify a method to use for custom object construction. Failure to provide a method for object construction results in a javax.json.bind.JsonbException being thrown. Serialization has no such requirement.

JSON-B can deserialize public, protected, and protected-static classes, but anonymous classes are not supported. The serialization of an anonymous class produces a JSON object. Interfaces are not supported during deserialization other than those mentioned already, and for serialization the runtime type is used.

Both serialization and deserialization support nested and static-nested classes for classes with public and protected access.

Support for JSON-P types

Because JSON-B is built on top of the JSON Processing API, it supports all JSON-P types. Those types are from the javax.json package and include all subtypes of JsonValue. Listing 22 shows that an instance of JsonArray serialized to a JSON array, Listing 23.

Listing 22. Serialization of a JSON Value
JsonArray value = Json.createArrayBuilder()
       .add(Json.createValue("John"))
       .add(JsonValue.NULL)
       .build();
Listing 23. JSON representation of a JSON Value instance
[
  "John",
  null
]

Property order

By default, properties are serialized in lexicographical order. An instance of the class in Listing 24 would be serialized as the JSON document shown in Listing 25.

Listing 24. Class with fields not is lexicographical order
public class LexicographicalOrder {
   public String dog = "Labradoodle";
   public String animal = "Cat";
   public String bread = "Chiapata";
   public String car = "Ford";
}
Listing 25. Serialization showing properties ordered lexicographically
{
  "animal": "Cat",
  "bread": "Chiapata",
  "car": "Ford",
  "dog": "Labradoodle"
}

JSON-B's property order strategy is configurable via a customization option. I discuss this in more detail in Part 3.

Default access strategy

As I briefly discussed in Part 1, JSON-B's default field access strategy expects the method or field to allow public access by specifying the public access modifier. During serialization, the public getter method for the field is called to retrieve its value. If no such getter exists (or if it doesn't have public access) then the field is accessed directly—although this only happens if the access modifier is public.

Likewise, deserialization relies on the public accessibility of the setter method to set property values. If the method does not exist or is not public, the public field is set directly.

You can configure JSON-B's field access strategy to be more restrictive by tweaking the relevant customization options. You'll learn more about this and other customizations in Part 3.

Conclusion

The JSON Binding API is sufficient out-of-the-box for most simple use-case scenarios, and comes with reasonable defaults. Using JSON-B will be intuitive for developers familiar with any other JSON binding technology, and it is accessible enough that developers new to JSON should easily pick it up.

You've had a taste of JSON-B and seen its major default features and functionality. To unharness the true power of JSON Binding you will sometimes need to customize those default operations. In Part 3 I introduce JSON-B's customization models and features. You'll learn how to use compile-time annotations and runtime configurations to customize nearly every aspect of the API, including low-level serialization and deserialization operations.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java development
ArticleID=1056391
ArticleTitle=Get started with the JSON Binding API, Part 2: Default mapping with JSON-B
publish-date=01032018