Serializar y deserializar una lista con Gson

1. Introducción

En este tutorial, exploraremos algunos casos avanzados de serialización y deserialización para List usando la biblioteca Gson de Google.

2. Lista de objetos

Un caso de uso común es serializar y deserializar una lista de POJO.

Considere la clase:

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

Así es como serializaríamos 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); }

Como podemos ver, la serialización es bastante sencilla.

Sin embargo, la deserialización es complicada. Aquí hay una forma incorrecta de hacerlo:

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

Aquí, aunque obtendríamos una Lista de tamaño dos, posterior a la deserialización, no sería una Lista de MyClass . Por lo tanto, la línea 6 arroja ClassCastException .

Gson puede serializar una colección de objetos arbitrarios, pero no puede deserializar los datos sin información adicional. Eso es porque no hay forma de que el usuario indique el tipo de objeto resultante. En cambio, mientras se deserializa, la colección debe ser de un tipo genérico específico.

La forma correcta de deserializar la lista sería:

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

Aquí, utilizamos TypeToken de Gson para determinar el tipo correcto que se deserializará: ArrayList . El idioma utilizado para obtener listOfMyClassObject en realidad define una clase interna local anónima que contiene un método getType () que devuelve el tipo completamente parametrizado.

3. Lista de objetos polimórficos

3.1. El problema

Considere un ejemplo de jerarquía de clases de animales:

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

¿Cómo serializamos y deserializamos List ? Podríamos usar TypeToken como usamos en la sección anterior. Sin embargo, Gson aún no podrá averiguar el tipo de datos concreto de los objetos almacenados en la lista.

3.2. Uso de deserializador personalizado

Una forma de resolver esto es agregar información de tipo al JSON serializado. Honramos ese tipo de información durante la deserialización JSON. Para esto, necesitamos escribir nuestro propio serializador y deserializador personalizado.

En primer lugar, presentaremos un nuevo campo String llamado type en la clase base Animal . Almacena el nombre simple de la clase a la que pertenece.

Echemos un vistazo a nuestras clases de muestra:

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 }

La serialización seguirá funcionando como antes sin ningún problema:

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

Para deserializar la lista, tendremos que proporcionar un deserializador personalizado:

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

Aquí, el mapa animalTypeRegistry mantiene el mapeo entre el nombre de la clase y el tipo de clase.

Durante la deserialización, primero extraemos el campo de tipo recién agregado . Usando este valor, hacemos una búsqueda en el mapa animalTypeRegistry para obtener el tipo de datos concreto. Este tipo de datos luego se pasa a fromJson () .

Veamos cómo usar nuestro deserializador personalizado:

@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. Usando RuntimeTypeAdapterFactory

Una alternativa a escribir un deserializador personalizado es usar la clase RuntimeTypeAdapterFactory presente en el código fuente de Gson. Sin embargo, la biblioteca no lo expone para que lo use el usuario . Por lo tanto, tendremos que crear una copia de la clase en nuestro proyecto Java.

Una vez hecho esto, podemos usarlo para deserializar nuestra lista:

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

Tenga en cuenta que el mecanismo subyacente sigue siendo el mismo.

Todavía necesitamos introducir la información de tipo durante la serialización. La información de tipo se puede utilizar posteriormente durante la deserialización. Por lo tanto, el tipo de campo sigue siendo necesario en todas las clases para que funcione esta solución. Simplemente no tenemos que escribir nuestro propio deserializador.

RuntimeTypeAdapterFactory proporciona el adaptador de tipo correcto según el nombre de campo que se le ha pasado y los subtipos registrados.

4. Conclusión

En este artículo, vimos cómo serializar y deserializar una lista de objetos usando Gson.

Como de costumbre, el código está disponible en GitHub.