«1. Введение

В этом руководстве мы рассмотрим несколько расширенных случаев сериализации и десериализации для списка с использованием библиотеки 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\u003cMyClass\u003e:

@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);
}

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

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

@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());
}

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

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

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

@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);
}

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

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

3.1. Проблема

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

public abstract class Animal {
    // ...
}

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

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

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

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

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

Во-первых, мы введем новое строковое поле с именем 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<Animal> 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<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);
    }
}

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

Здесь карта 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<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. Использование RuntimeTypeAdapterFactory

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

@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);
}

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

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

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

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

4. Заключение

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