1. Введение
В этом руководстве мы рассмотрим несколько расширенных случаев сериализации и десериализации для списка
с использованием библиотеки Gson от Google .
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<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);
}
Как мы видим, сериализация довольно проста.
Однако десериализация сложна. Вот неправильный способ сделать это:
@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<MyClass>
. Идиома, используемая для получения listOfMyClassObject
, на самом деле определяет анонимный локальный внутренний класс, содержащий метод getType()
, возвращающий полностью параметризованный тип.
3. Список полиморфных объектов
3.1. Проблема
Рассмотрим пример иерархии классов животных:
public abstract class Animal {
// ...
}
public class Dog extends Animal {
// ...
}
public class Cow extends Animal {
// ...
}
Как сериализовать и десериализовать List<Animal>
? Мы могли бы использовать TypeToken<ArrayList<Animal>>
, как мы использовали в предыдущем разделе. Однако 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<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.
Как обычно, код доступен на GitHub .