Сериализация и десериализация списка с помощью Gson

1. Введение

В этом руководстве мы рассмотрим несколько расширенных случаев сериализации и десериализации для List с использованием библиотеки Google Gson.

2. Список объектов

Один из распространенных вариантов использования - сериализация и десериализация списка POJO.

Рассмотрим класс:

public class MyClass { private int id; private String name; public MyClass(int id, String name) { this.id = id; this.name = name; } // getters and setters }

Вот как мы будем сериализовать List :

@Test public void givenListOfMyClass_whenSerializing_thenCorrect() { List 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); }

Как мы видим, сериализация довольно проста.

Однако десериализация - непростая задача. Вот неправильный способ сделать это:

@Test(expected = ClassCastException.class) public void givenJsonString_whenIncorrectDeserializing_thenThrowClassCastException() { String inputString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]"; Gson gson = new Gson(); List outputList = gson.fromJson(inputString, ArrayList.class); assertEquals(1, outputList.get(0).getId()); }

Здесь, хотя мы хотели бы получить список размером два, после десериализации, она не была бы список из MyClass . Следовательно, строка №6 выдает исключение ClassCastException .

Gson может сериализовать коллекцию произвольных объектов, но не может десериализовать данные без дополнительной информации. Это потому, что пользователь не может указать тип результирующего объекта. Вместо этого при десериализации Коллекция должна иметь определенный универсальный тип.

Правильный способ десериализации списка :

@Test public void givenJsonString_whenDeserializing_thenReturnListOfMyClass() { String inputString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]"; List inputList = Arrays.asList(new MyClass(1, "name1"), new MyClass(2, "name2")); Type listOfMyClassObject = new TypeToken
    
     () {}.getType(); Gson gson = new Gson(); List outputList = gson.fromJson(inputString, listOfMyClassObject); assertEquals(inputList, outputList); }
    

Здесь мы используем TypeToken Gson для определения правильного типа для десериализации - ArrayList . Идиома, используемая для получения listOfMyClassObject, фактически определяет анонимный локальный внутренний класс, содержащий метод getType (), который возвращает полностью параметризованный тип.

3. Список полиморфных объектов.

3.1. Проблема

Рассмотрим пример иерархии классов животных:

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

Как мы сериализуем и десериализуем List ? Мы могли бы использовать TypeToken как мы использовали в предыдущем разделе. Однако Gson по-прежнему не сможет определить конкретный тип данных объектов, хранящихся в списке.

3.2. Использование специального десериализатора

Один из способов решить эту проблему - добавить информацию о типе в сериализованный JSON. Мы учитываем эту информацию о типе во время десериализации JSON. Для этого нам нужно написать собственный сериализатор и десериализатор.

Во-первых, мы представим новое поле String под названием type в базовом классе Animal . Он хранит простое имя класса, к которому он принадлежит.

Давайте посмотрим на наши образцы классов:

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 }

Сериализация продолжит работать, как и раньше, без каких-либо проблем:

@Test public void givenPolymorphicList_whenSerializeWithTypeAdapter_thenCorrect() { String expectedString = "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]"; List inList = new ArrayList(); inList.add(new Dog()); inList.add(new Cow()); String jsonString = new Gson().toJson(inList); assertEquals(expectedString, jsonString); }

Чтобы десериализовать список, нам нужно предоставить собственный десериализатор:

public class AnimalDeserializer implements JsonDeserializer { private String animalTypeElementName; private Gson gson; private Map
    
      animalTypeRegistry; public AnimalDeserializer(String animalTypeElementName) { this.animalTypeElementName = animalTypeElementName; this.gson = new Gson(); this.animalTypeRegistry = new HashMap(); } public void registerBarnType(String animalTypeName, Class animalType) { animalTypeRegistry.put(animalTypeName, animalType); } public Animal deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { JsonObject animalObject = json.getAsJsonObject(); JsonElement animalTypeElement = animalObject.get(animalTypeElementName); Class animalType = animalTypeRegistry.get(animalTypeElement.getAsString()); return gson.fromJson(animalObject, animalType); } }
    

Здесь карта animalTypeRegistry поддерживает соответствие между именем класса и типом класса.

Во время десериализации мы сначала извлекаем только что добавленное поле типа . Используя это значение, мы ищем в карте animalTypeRegistry, чтобы получить конкретный тип данных. Затем этот тип данных передается в fromJson () .

Давайте посмотрим, как использовать наш собственный десериализатор:

@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 outList = gson.fromJson(inputString, new TypeToken
    
     (){}.getType()); assertEquals(2, outList.size()); assertTrue(outList.get(0) instanceof Dog); assertTrue(outList.get(1) instanceof Cow); }
    

3.3. Использование RuntimeTypeAdapterFactory

Альтернативой написанию настраиваемого десериализатора является использование класса RuntimeTypeAdapterFactory, присутствующего в исходном коде Gson. Однако он не предоставляется библиотекой для использования пользователем . Следовательно, нам нужно создать копию класса в нашем Java-проекте.

Как только это будет сделано, мы можем использовать его для десериализации нашего списка:

@Test public void givenPolymorphicList_whenDeserializeWithRuntimeTypeAdapter_thenCorrect() { String inputString = "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]"; Type listOfAnimals = new TypeToken
    
     (){}.getType(); RuntimeTypeAdapterFactory adapter = RuntimeTypeAdapterFactory.of(Animal.class, "type") .registerSubtype(Dog.class) .registerSubtype(Cow.class); Gson gson = new GsonBuilder().registerTypeAdapterFactory(adapter).create(); List outList = gson.fromJson(inputString, listOfAnimals); assertEquals(2, outList.size()); assertTrue(outList.get(0) instanceof Dog); assertTrue(outList.get(1) instanceof Cow); }
    

Обратите внимание, что основной механизм все тот же.

Нам все еще нужно ввести информацию о типе во время сериализации. Информация о типе позже может быть использована во время десериализации. Следовательно, для работы этого решения в каждом классе по-прежнему требуется тип поля . Нам просто не нужно писать собственный десериализатор.

RuntimeTypeAdapterFactory предоставляет правильный адаптер типа на основе переданного ему имени поля и зарегистрированных подтипов.

4. Вывод

В этой статье мы увидели, как сериализовать и десериализовать список объектов с помощью Gson.

Как обычно, код доступен на GitHub.