Jackson Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we'll explore a few advanced serialization and deserialization cases for List using Google's Gson library.

2. List of Objects

One common use case is to serialize and deserialize a list of POJOs.

Consider the class:

public class MyClass {
    private int id;
    private String name;

    public MyClass(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // getters and setters
}

Here's how we would serialize List<MyClass>:

@Test
public void givenListOfMyClass_whenSerializing_thenCorrect() {
    List<MyClass> list = Arrays.asList(new MyClass(1, "name1"), new MyClass(2, "name2"));

    Gson gson = new Gson();
    String jsonString = gson.toJson(list);
    String expectedString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]";

    assertEquals(expectedString, jsonString);
}

As we can see, serialization is fairly straightforward.

However, deserialization is tricky. Here's an incorrect way of doing it:

@Test(expected = ClassCastException.class)
public void givenJsonString_whenIncorrectDeserializing_thenThrowClassCastException() {
    String inputString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]";

    Gson gson = new Gson();
    List<MyClass> outputList = gson.fromJson(inputString, ArrayList.class);

    assertEquals(1, outputList.get(0).getId());
}

Here, although we would get a List of size two, post-deserialization, it wouldn't be a List of MyClass. Therefore, line #6 throws ClassCastException.

Gson can serialize a collection of arbitrary objects but can't deserialize the data without additional information. That's because there's no way for the user to indicate the type of the resulting object. Instead, while deserializing, the Collection must be of a specific, generic type.

The correct way to deserialize the List would be:

@Test
public void givenJsonString_whenDeserializing_thenReturnListOfMyClass() {
    String inputString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]";
    List<MyClass> inputList = Arrays.asList(new MyClass(1, "name1"), new MyClass(2, "name2"));

    Type listOfMyClassObject = new TypeToken<ArrayList<MyClass>>() {}.getType();

    Gson gson = new Gson();
    List<MyClass> outputList = gson.fromJson(inputString, listOfMyClassObject);

    assertEquals(inputList, outputList);
}

Here, we use Gson's TypeToken to determine the correct type to be deserialized – ArrayList<MyClass>. The idiom used to get the listOfMyClassObject actually defines an anonymous local inner class containing a method getType() that returns the fully parameterized type.

3. List of Polymorphic Objects

3.1. The Problem

Consider an example class hierarchy of animals:

public abstract class Animal {
    // ...
}

public class Dog extends Animal {
    // ...
}

public class Cow extends Animal {
    // ...
}

How do we serialize and deserialize List<Animal>? We could use TypeToken<ArrayList<Animal>> like we used in the previous section. However, Gson still won't be able to figure out the concrete data type of the objects stored in the list.

3.2. Using Custom Deserializer

One way to solve this is to add type information to the serialized JSON. We honor that type information during JSON deserialization. For this, we need to write our own custom serializer and deserializer.

Firstly, we'll introduce a new String field called type in the base class Animal. It stores the simple name of the class to which it belongs.

Let's take a look at our sample classes:

public abstract class Animal {
    public String type = "Animal";
}
public class Dog extends Animal {
    private String petName;

    public Dog() {
        petName = "Milo";
        type = "Dog";
    }

    // getters and setters
}
public class Cow extends Animal {
    private String breed;

    public Cow() {
        breed = "Jersey";
        type = "Cow";
    }

    // getters and setters
}

Serialization will continue to work as before without any issues:

@Test 
public void givenPolymorphicList_whenSerializeWithTypeAdapter_thenCorrect() {
    String expectedString
      = "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]";

    List<Animal> inList = new ArrayList<>();
    inList.add(new Dog());
    inList.add(new Cow());

    String jsonString = new Gson().toJson(inList);

    assertEquals(expectedString, jsonString);
}

In order to deserialize the list, we'll have to provide a custom deserializer:

public class AnimalDeserializer implements JsonDeserializer<Animal> {
    private String animalTypeElementName;
    private Gson gson;
    private Map<String, Class<? extends Animal>> animalTypeRegistry;

    public AnimalDeserializer(String animalTypeElementName) {
        this.animalTypeElementName = animalTypeElementName;
        this.gson = new Gson();
        this.animalTypeRegistry = new HashMap<>();
    }

    public void registerBarnType(String animalTypeName, Class<? extends Animal> animalType) {
        animalTypeRegistry.put(animalTypeName, animalType);
    }

    public Animal deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
        JsonObject animalObject = json.getAsJsonObject();
        JsonElement animalTypeElement = animalObject.get(animalTypeElementName);

        Class<? extends Animal> animalType = animalTypeRegistry.get(animalTypeElement.getAsString());
        return gson.fromJson(animalObject, animalType);
    }
}

Here, the animalTypeRegistry map maintains the mapping between the class name and the class type.

During deserialization, we first extract out the newly added type field. Using this value, we do a lookup on the animalTypeRegistry map to get the concrete data type. This data type is then passed to fromJson().

Let's see how to use our custom deserializer:

@Test
public void givenPolymorphicList_whenDeserializeWithTypeAdapter_thenCorrect() {
    String inputString
      = "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]";

    AnimalDeserializer deserializer = new AnimalDeserializer("type");
    deserializer.registerBarnType("Dog", Dog.class);
    deserializer.registerBarnType("Cow", Cow.class);
    Gson gson = new GsonBuilder()
      .registerTypeAdapter(Animal.class, deserializer)
      .create();

    List<Animal> outList = gson.fromJson(inputString, new TypeToken<List<Animal>>(){}.getType());

    assertEquals(2, outList.size());
    assertTrue(outList.get(0) instanceof Dog);
    assertTrue(outList.get(1) instanceof Cow);
}

3.3. Using RuntimeTypeAdapterFactory

An alternative to writing a custom deserializer is to use the RuntimeTypeAdapterFactory class present in the Gson source code. However, it's not exposed by the library for the user to use. Hence, we'll have to create a copy of the class in our Java project.

Once this is done, we can use it to deserialize our list:

@Test
public void givenPolymorphicList_whenDeserializeWithRuntimeTypeAdapter_thenCorrect() {
    String inputString
      = "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]";

    Type listOfAnimals = new TypeToken<ArrayList<Animal>>(){}.getType();

    RuntimeTypeAdapterFactory<Animal> adapter = RuntimeTypeAdapterFactory.of(Animal.class, "type")
      .registerSubtype(Dog.class)
      .registerSubtype(Cow.class);

    Gson gson = new GsonBuilder().registerTypeAdapterFactory(adapter).create();

    List<Animal> outList = gson.fromJson(inputString, listOfAnimals);

    assertEquals(2, outList.size());
    assertTrue(outList.get(0) instanceof Dog);
    assertTrue(outList.get(1) instanceof Cow);
}

Note that the underlying mechanism is still the same.

We still need to introduce the type information during serialization. The type information can later be used during deserialization. Hence, the field type is still required in every class for this solution to work. We just don't have to write our own deserializer.

RuntimeTypeAdapterFactory provides the correct type adapter based on the field name passed to it and the registered subtypes.

4. Conclusion

In this article, we saw how to serialize and deserialize a list of objects using Gson.

As usual, the code is available over on GitHub.

Jackson bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!