GSON DESERIALISER EXAMPLE

来源:互联网 发布:淘宝店商品怎么分类 编辑:程序博客网 时间:2024/05/17 05:11

转自:http://www.javacreed.com/gson-deserialiser-example/


GSON DESERIALISER EXAMPLE



This article continues on a previous article, that described simple and basic use of Gson. In this article we will see how to parse complex JSON objects into existing Java objects that do not necessary have the same structure as the JSON object. We will see how the use of the Gson deserialiser (JsonDeserializer Java Doc) in order to control how the JSON object maps to the Java object.

Observation

Please note the we will use the terms parse or deserialise interchangeably in this article.

All code listed below is available at: https://java-creed-examples.googlecode.com/svn/gson/Gson Deserialiser Example. Most of the examples will not contain the whole code and may omit fragments which are not relevant to the example being discussed. The readers can download or view all code from the above link.

The readers are encouraged to first read the article Simple Gson Example before proceeding, unless they are already familiar with Gson.

A Simple Example

Let’s say we have the following JSON object, where it contains a popular Java book (Amazon) title written by two, well known, authors.

{  'title':    'Java Puzzlers: Traps, Pitfalls, and Corner Cases',  'isbn-10':  '032133678X',  'isbn-13':  '978-0321336781',  'authors':  ['Joshua Bloch', 'Neal Gafter']}

The above JSON comprise four fields, one of which is an array. These fields represent our book. Using the methods discussed in theSimple Gson Example article would create a problem. By default, Gson expects to find variable names in Java with the same name as that found in JSON. Therefore, we should have a class with the following field names: titleisbn-10isbn-13 and authors. But names in Java cannot contain the minus sign (-) as described in the Java Language Specification (Chapter 6).

Using the JsonDeserializer, we have full control over how JSON is parsed as we will see in the following example. Alternatively we can use annotations as described in Gson Annotations Example article. Annotations provide less control, but are simpler to use and understand. With that said, annotations have their limitations too and cannot address all problems described here.

Consider the following simple Java object.

package com.javacreed.examples.gson.part1;public class Book {  private String[] authors;  private String isbn10;  private String isbn13;  private String title;  // Methods removed for brevity}

This Java object will be used to hold the book listed in the JSON object shown earlier. Note that JSON object has four fields, one for each field found in JSON. The structure of these two objects (Java and JSON) is the same in this example but this is not required. The Java object can have a different structure than that found in the corresponding JSON object.

In order to be able to parse JSON to Java we need to create our own instance of the JsonDeserializer interface and register this with theGsonBuilder (Java Doc). The following example shows our implementation of JsonDeserializer.

package com.javacreed.examples.gson.part1;import java.lang.reflect.Type;import com.google.gson.JsonArray;import com.google.gson.JsonDeserializationContext;import com.google.gson.JsonDeserializer;import com.google.gson.JsonElement;import com.google.gson.JsonObject;import com.google.gson.JsonParseException;public class BookDeserializer implements JsonDeserializer<Book> {  @Override  public Book deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)      throws JsonParseException {      //The deserialisation code is missing    final Book book = new Book();    book.setTitle(title);    book.setIsbn10(isbn10);    book.setIsbn13(isbn13);    book.setAuthors(authors);    return book;  }}

The above example is not complete and we still need to add the most important thing, which is the deserialisation. Let’s understand this class before we make it more complex by adding more code to it.

The interface JsonDeserializer requires a type, which is the type of object that we will be parsed. In this case, we are parsing JSON into the Java object of type Book. The return type of the deserialize() method must be of the same type as the generics parameter,Book.

Gson will parse the JSON object into a Java object of type JsonElement (Java Doc). An instance of JsonElement can be one of the following:

  • JsonPrimitive (Java Doc) – such as a string or integer
  • JsonObject (Java Doc) – a collection of JsonElements indexed by thier name (of type String). This is similar to a Map<String, JsonElement> (Java Doc)
  • JsonArray (Java Doc) – a collection of JsonElements. Note that the array elements can be any of the four types and mixed types are supported.
  • JsonNull (Java Doc) – a null value
Types of JsonElement

Types of JsonElement

The above image shows all types of JsonElement. The JsonObject can be thought of a collection of name/value pairs where the values are of type JsonElement. The following image shows an example of JSON object hierarchy.

Json Object Hierarchy

Json Object Hierarchy

The above image shows a JSON object hierarchy with a JsonObject as its root. It is important to note that, different from Java, JSON supports arrays of different types. In the above image, the JsonArray comprises JsonObjects, JsonArrays and JsonPrimitives. Please note that the JSON object hierarchy shown above does not reflect the JSON object listed before. Following is the JSON object hierarchy for the JSON object listed before.

Book Json Object Hierarchy

Book Json Object Hierarchy

If we are to deserialise this JSON object, we first need to convert the given JsonElement into a JsonObject as shown next.

// The variable 'json' is passed as a parameter to the deserialize() methodfinal JsonObject jsonObject = json.getAsJsonObject();

JsonElement can be converted to any of the other types using a similar approach.

The elements within the a JsonObject can be retrieved by their name. For example, to retrieve the title element from the JSON listed above we can do the following.

// The variable 'json' is passed as a parameter to the deserialize() methodfinal JsonObject jsonObject = json.getAsJsonObject();JsonElement titleElement = jsonObject.get("title")

The object returned is not a String but yet another JsonElement. This can be converted to String by invoking the getAsString() as shown below.

// The variable 'json' is passed as a parameter to the deserialize() methodfinal JsonObject jsonObject = json.getAsJsonObject();JsonElement titleElement = jsonObject.get("title")final String title = jsonTitle.getAsString();

The following example shows how to convert the JSON object listed above using a custom deserializer.

package com.javacreed.examples.gson.part1;import java.lang.reflect.Type;import com.google.gson.JsonArray;import com.google.gson.JsonDeserializationContext;import com.google.gson.JsonDeserializer;import com.google.gson.JsonElement;import com.google.gson.JsonObject;import com.google.gson.JsonParseException;public class BookDeserializer implements JsonDeserializer<Book> {  @Override  public Book deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)      throws JsonParseException {    final JsonObject jsonObject = json.getAsJsonObject();    final JsonElement jsonTitle = jsonObject.get("title");    final String title = jsonTitle.getAsString();    final String isbn10 = jsonObject.get("isbn-10").getAsString();    final String isbn13 = jsonObject.get("isbn-13").getAsString();    final JsonArray jsonAuthorsArray = jsonObject.get("authors").getAsJsonArray();    final String[] authors = new String[jsonAuthorsArray.size()];    for (int i = 0; i < authors.length; i++) {      final JsonElement jsonAuthor = jsonAuthorsArray.get(i);      authors[i] = jsonAuthor.getAsString();    }    final Book book = new Book();    book.setTitle(title);    book.setIsbn10(isbn10);    book.setIsbn13(isbn13);    book.setAuthors(authors);    return book;  }}

In the above example, we are retrieving the JSON element and its four fields and returning an instance of Book.

Before we can utilise our new deserializer, we must instruct Gson to use our deserializer when parsing objects of type Book, as shown in the next code example.

package com.javacreed.examples.gson.part1;import java.io.InputStreamReader;import com.google.gson.Gson;import com.google.gson.GsonBuilder;public class Main {  public static void main(String[] args) throws Exception {    // Configure Gson    GsonBuilder gsonBuilder = new GsonBuilder();    gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());    Gson gson = gsonBuilder.create();    // The JSON data    try(Reader reader = new InputStreamReader(Main.class.getResourceAsStream("/part1/sample.json"), "UTF-8")){      // Parse JSON to Java      Book book = gson.fromJson(reader, Book.class);      System.out.println(book);    }  }}

In the above example, we are creating an instance of Gson through the GsonBuilder. Using the registerTypeAdapter() method, we are registering our deserializer and instructing Gson to use our deserializer when deserializing objects of type Book. When we request Gson to deserialize an object to the Book class, Gson will use our deserializer. The following steps describes what happens when we invoke: gson.fromJson(data, Book.class).

  1. Parse the input as JsonElement. Note that even though the type of the object is JsonElement, this can be anything. At this stage, the string JSON object is deserialised into Java objects of type JsonElements. This step also ensures that the given JSON data is valid.
  2. Find the deserializer for the given object, in this case the BookDeserializer instance.
  3. Invokes the method deserialize() and provides the necessary parameters. In this example, our deserialize() will be invoked. Here an object of type Book is created from the given JsonElement object. This is from Java to Java conversion.
  4. Returns the object returned by the deserialize() method to the caller of the fromJson() method. This is like a chain, where Gson receives an object from our deserializer and returns it to its caller.

Running the above example would print the following:

Java Puzzlers: Traps, Pitfalls, and Corner Cases   [ISBN-10: 032133678X] [ISBN-13: 978-0321336781]Written by:  >> Joshua Bloch  >> Neal Gafter

This concludes our simple example. This example acts as a primer for following, more complex, examples. In the next example we will discuss an enhanced version of the objects discussed here, where the authors are not a simple string but an object.

Nested Objects

In this example we will describe how to deserialise nested objects, that is, objects within other objects. Here we will introduce a new entity, the author. A book, together with the title and ISBN can have a list of authors. On the other hand every author can have many books. The JSON object that will be using in this example differs from the previous one to cater for the new entity as shown next:

{  'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',  'isbn': '032133678X',  'authors':[    {      'id': 1,      'name': 'Joshua Bloch'    },    {      'id': 2,      'name': 'Neal Gafter'    }  ]}

The structure of the JSON object was changed slightly and instead of primitives we have the authors as JSON objects as shown in the following image.

Book and Authors Json Objects Hierarchy

Book and Authors Json Objects Hierarchy

We still have one book, only this time we have a more complex and detailed JSON object. Together with a name, the author also has anid. A new class called Author is added to the model and the Book class now uses it to save the author information. This immediately leads to the following question.

How should we deserialise the new Author class?
There are several options.

  1. We can update the BookDeserializer and add the authors’ deserialisation code there. This has a limitation as it binds the deserialization of the Author with that of the Book, and thus is not recommended.
  2. We can use the default Gson implementation, which will work well in this case as both the Java object (the Author class) and the JSON objects have the same fields’ names and can be deserialised as described in the article Simple Gson Example.
  3. Alternatively, we can write an AuthorDeserializer class which will take care of the deserialisation of authors.

We will start with the second option, to keep the changes to a minimum and the example as simple as possible. Then we add the new deserializer to show the flexibility of Gson.

The JsonDeserializer provides an instance of JsonDeserializationContext (Java Doc) as the third parameter to the deserialize()method. We have not yet used this parameter. We can delegate the deserialisation of objects to the given instance ofJsonDeserializationContext. It will deserialise the given JsonElement and returns an instance of the required type as shown below.

Author author = context.deserialize(jsonElement, Author.class); 

The example shown above, is delegating the deserialisation of the Author class to the context. In turn it will try to locate an instance ofJsonDeserializer that can deserialize it, if one is registered, otherwise it uses the default mechanism as described in the article titled:Simple Gson Example.

Our example uses an array of Authors and thus we need to use the correct type as shown in the following example.

package com.javacreed.examples.gson.part2;import java.lang.reflect.Type;import com.google.gson.JsonArray;import com.google.gson.JsonDeserializationContext;import com.google.gson.JsonDeserializer;import com.google.gson.JsonElement;import com.google.gson.JsonObject;import com.google.gson.JsonParseException;public class BookDeserializer implements JsonDeserializer<Book> {  @Override  public Book deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)      throws JsonParseException {   final JsonObject jsonObject = json.getAsJsonObject();    final String title = jsonObject.get("title").getAsString();    final String isbn10 = jsonObject.get("isbn-10").getAsString();    final String isbn13 = jsonObject.get("isbn-13").getAsString();    // Delegate the deserialization to the context    Author[] authors = context.deserialize(jsonObject.get("authors"), Author[].class);    final Book book = new Book();    book.setTitle(title);    book.setIsbn10(isbn10);    book.setIsbn13(isbn13);    book.setAuthors(authors);    return book;  }}

Converting from a JsonPrimitive to a JsonObject was very easy and straightforward as we saw in the above example.

Like the BookDeserialiser, we can write the ArthurDeserialiser and deserialise the author in a similar fashion we did with the book as shown next,

package com.javacreed.examples.gson.part2;import java.lang.reflect.Type;import com.google.gson.JsonDeserializationContext;import com.google.gson.JsonDeserializer;import com.google.gson.JsonElement;import com.google.gson.JsonObject;import com.google.gson.JsonParseException;public class AuthorDeserializer implements JsonDeserializer {  @Override  public Author deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)      throws JsonParseException {    final JsonObject jsonObject = json.getAsJsonObject();    final Author author = new Author();    author.setId(jsonObject.get("id").getAsInt());    author.setName(jsonObject.get("name").getAsString());    return author;  }}

In order to use the ArthurDeserialiser we need to add this to the GsonBuilder as shown next,

package com.javacreed.examples.gson.part2;import java.io.IOException;import java.io.InputStreamReader;import java.io.Reader;import com.google.gson.Gson;import com.google.gson.GsonBuilder;public class Main {  public static void main(final String[] args) throws IOException {    // Configure GSON    final GsonBuilder gsonBuilder = new GsonBuilder();    gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());    gsonBuilder.registerTypeAdapter(Author.class, new AuthorDeserializer());    final Gson gson = gsonBuilder.create();    // Read the JSON data    try (Reader reader = new InputStreamReader(Main.class.getResourceAsStream("/part2/sample.json"), "UTF-8")) {      // Parse JSON to Java      final Book book = gson.fromJson(reader, Book.class);      System.out.println(book);    }  }}

There is no need to change the BookDeserialiser as the deserialisation of the author is delegated to the context. This is another advantage of using the context to deserialise the other/nested objects. Running the above code will produce the following.

Java Puzzlers: Traps, Pitfalls, and Corner Cases [032133678X]Written by:  >> [1] Joshua Bloch  >> [2] Neal Gafter

This concludes this section about nested objects. In the next section we will see how to refer to JSON objects located elsewhere in the JSON object tree.

Object References

Consider the following JSON.

{  'authors': [    {      'id': 1,      'name': 'Joshua Bloch'    },    {      'id': 2,      'name': 'Neal Gafter'    }  ],  'books': [    {      'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',      'isbn': '032133678X',      'authors':[1, 2]    },    {      'title': 'Effective Java (2nd Edition)',      'isbn': '0321356683',       'authors':[1]    }  ]}

The JSON object shown above comprise two authors and two books. The books have a reference to the authors through their ids as in this example the books’ field authors only contains the authors’ ids. This is quite a common scenario as this approach will reduce the size of the JSON object as duplicate objects are referred to by their id. The following image captures the new JSON object hierarchy.

New JSON Object Hierarchy

New JSON Object Hierarchy

This resembles relational database tables (Wiki), where the book has a foreign key (Wiki) to the authors table. This new JSON object structure introduces new challenges that need to be addressed. When deserialising the books, we need to get hold of the authors that were deserialised from other branch of the JSON object hierarchy. The book only has the author id. The rest of the author information is found elsewhere, out of scope of the current context.

There are various approaches to this problem, some of which are listed below.

  1. One approach is to use a two stage processing. First we parse JSON to Java, that is deserialise the books and authors as these appear in JSON. The book class will contain an array of authors’ ids and not an array or authors. Then, in the second stage, we associate the objects and set the authors to their books. The following image shows the flow described here.
    Two Stage Processing

    Two Stage Processing

    This approach requires more classes but provides a great deal of flexibility and provides better separation of concerns. We need to create a set of classes that simply represent the JSON objects in Java, then we need another set of classes that matches our requirements (model). In this example, we have one Book class and another Author class, two in total. Using this approach, we will end up with four classes, two for the book and another two for the author. While in this example, it may seem feasible, this becomes more complex when we have tens, if not hundreds of classes.

  2. Another approach is to provide the BookDeserialiser class with all authors and then have the deserialiser retrieving the authors from this shared object. This approach removes the need of the middle stage as the JSON objects are deserialised into proper Java objects without passing through an intermediate stage.
    Shared Object between Deserialisers

    Shared Object between Deserialisers

    While this approach may sound appealing, it requires that the BookDeserialiser and the AuthorDeserialiser share a common object. Furthermore, when retrieving authors the BookDeserialiser has to refer to this shared object instead of using theJsonDeserializationContext as we did before. This approach requires changes in several places. The deserialisers need to be modified and the main() method too.

  3. The AuthorDeserialiser can be made such that it caches the deserialised authors and return these next time requested by their id. This approach is quite attractive as it leverages the use of JsonDeserializationContext and make the relationship transparent. Unfortunately it adds in complexity as the AuthorDeserialiser needs to handle the caching. With that said, this approach requires the least changes as only the AuthorDeserialiser is modified.
    AuthorDeserialiser using Cache

    AuthorDeserialiser using Cache

    As shown in the image above, only the AuthorDeserialiser accesses the cache object. The rest of the system is unaware of this.

All approaches are feasible and all have their advantages and disadvantages. We will use the third approach as it is the one that has the least impact on the project.

Observation

In theory, the first approach provides better separation of concerns when compared with the other two. We can have the association logic within the new Data class. But this requires many changes when compared with the third approach. That was the reason behind the use of the third approach over the other two. Always consider the effort required to make a change and try to minimise it.

The JSON object shown above contains two arrays. We need to have a new Java class that reflects this JSON object.

package com.javacreed.examples.gson.part3;public class Data {  private Author[] authors;  private Book[] books;  // Methods removed for brevity}

The fields order determines which of the two sets is deserialised first. This does not matter in our case and the books can be deserialised before the authors as we will see later on.

The AuthorDeserialiser need to be modified such that it caches the authors that were deserialised as shown next.

package com.javacreed.examples.gson.part3;import java.lang.reflect.Type;import java.util.HashMap;import java.util.Map;import com.google.gson.JsonDeserializationContext;import com.google.gson.JsonDeserializer;import com.google.gson.JsonElement;import com.google.gson.JsonObject;import com.google.gson.JsonParseException;import com.google.gson.JsonPrimitive;public class AuthorDeserializer implements JsonDeserializer<Author> {  private final ThreadLocal<Map<Integer, Author>> cache = new ThreadLocal<Map<Integer, Author>>() {    @Override    protected Map<Integer, Author> initialValue() {      return new HashMap<>();    }  };  @Override  public Author deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)      throws JsonParseException {    // Only the ID is available    if (json.isJsonPrimitive()) {      final JsonPrimitive primitive = json.getAsJsonPrimitive();      return getOrCreate(primitive.getAsInt());    }     // The whole object is available    if (json.isJsonObject()) {      final JsonObject jsonObject = json.getAsJsonObject();      final Author author = getOrCreate(jsonObject.get("id").getAsInt());      author.setName(jsonObject.get("name").getAsString());      return author;    }    throw new JsonParseException("Unexpected JSON type: " + json.getClass().getSimpleName());  }  private Author getOrCreate(final int id) {    Author author = cache.get().get(id);    if (author == null) {      author = new Author();      author.setId(id);      cache.get().put(id, author);    }    return author;  }}

We made several changes in this class. Let us go through these changes one by one.

  1. The authors are stored in the following object.
      private final ThreadLocal<Map<Integer, Author>> cache = new ThreadLocal<Map<Integer, Author>>() {    @Override    protected Map<Integer, Author> initialValue() {      return new HashMap<>();    }  };

    It uses a Map<String, Object> as cache mechanism. The map is saved within a
    ThreadLocal (JavaDoc) instance to isolate state between the multiple threads. This class allows multiple threads to use the same variable without interfering with the other threads.

    Observation

    Please note that while this approach is thread-safe, it may not fulfil the needs of specific application domain needs and thus one may have to use a different approach. Please refer to the articles: How to Cache Results to Boost Performance and Caching Made Easy with Spring for more caching examples.

  2. The authors are always retrieved using the following method.
      private Author getOrCreate(final int id) {    Author author = cache.get().get(id);    if (author == null) {      author = new Author();      cache.get().put(id, author);    }    return author;  }

    This method first tries to obtain the authors instance from the cache. If no author is found with the given id, then one is created and added to the cache.

    This approach allows us to create the author with just its id, and then populate its contents later on when they become available. That is why the order of deserialisation does not affect the outcome. We can first deserialise the books and then the authors. In this case, first the authors are created with just their ids and the their names are added.

  3. The deserialize() was modified to handle the new requirements. Since many changes were applied, we will split this method further and describe each part separately.

    The descerialiser can receive either a JsonPrimitive or a JsonObject. When the BookDeserialiser executes the following code, the JsonElement passed to the AuthorDeserialiser will be an instance of JsonPrimitive.

        // This is executed within the BookDeserialiser    Author[] authors = context.deserialize(jsonObject.get("authors"), Author[].class);

    The following image shows this process.

    Delegating Deserialisation to Context

    Delegating Deserialisation to Context

    The BookDeserialiser delegates the deseialisation of the Authors array to the context, and provides an array of integers. For each integer, the context invokes the deserialize() method of the AuthorDeserialiser and pass the integer as a JsonPrimitive.

    On the other hand, when the authors are deserialised, we will receive an instance of JsonObject containing the author and his/her details. Therefore, before we convert the given JsonElement, we need to determine whether it is of the correct type.

        // Only the ID is available    if (json.isJsonPrimitive()) {      final JsonPrimitive primitive = json.getAsJsonPrimitive();      final Author author = getOrCreate(primitive.getAsInt());      return author;    } 

    In the above example, only the id was available. The JsonElement is converted into a JsonPrimitive and then into an int. Thisint is used to retrieve the author from the getOrCreate() method.

    The JsonElement can be of type JsonObject as shown next.

        // The whole object is available    if (json.isJsonObject()) {      final JsonObject jsonObject = json.getAsJsonObject();      final Author author = getOrCreate(jsonObject.get("id").getAsInt());      author.setName(jsonObject.get("name").getAsString());      return author;    }

    In this case, the name is added to the Author instance returned by the getOrCreate() method, before the author is returned.

    Finally, if the given JsonElement instance is neither a JsonPrimitive nor a JsonObject, an exception is thrown to indicate that the given type is not supported.

        throw new JsonParseException("Unexpected JSON type: " + json.getClass().getSimpleName());

The above captures all required changes to accommodate and address the new challenges. The BookDeserialiser and the main()method do not require any changes. Running the main() will produce the following.

Output missing...

This example concludes our article about the Gson desieralizer. Using a custom deserializer is not difficult and enables us to work with different JSON representations with little effort. Note that the Java domain objects do not need to correspond to the JSON object being parsed. Furthermore, we can use existing Java objects with new JSON representations. Some problems may be more challenging than others to solve. Try to keep the changes to the existing code to a minimum and your design as flexible as required (not as possible).


0 0
原创粉丝点击