Serializzazione e deserializzazione di un elenco con Gson

1. Introduzione

In questo tutorial, esploreremo alcuni casi avanzati di serializzazione e deserializzazione per List utilizzando la libreria Gson di Google.

2. Elenco di oggetti

Un caso d'uso comune è serializzare e deserializzare un elenco di POJO.

Considera la classe:

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

Ecco come serializzare l' elenco :

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

Come possiamo vedere, la serializzazione è abbastanza semplice.

Tuttavia, la deserializzazione è complicata. Ecco un modo errato per farlo:

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

Anche se avremmo ottenuto un elenco di dimensione due, post-deserializzazione, non sarebbe un elenco di MyClass . Pertanto, la riga # 6 genera ClassCastException .

Gson può serializzare una raccolta di oggetti arbitrari ma non può deserializzare i dati senza informazioni aggiuntive. Questo perché non c'è modo per l'utente di indicare il tipo di oggetto risultante. Al contrario, durante la deserializzazione, la raccolta deve essere di un tipo specifico e generico.

Il modo corretto per deserializzare l' elenco sarebbe:

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

Qui, usiamo TypeToken di Gson per determinare il tipo corretto da deserializzare: ArrayList . L'idioma utilizzato per ottenere listOfMyClassObject definisce effettivamente una classe interna locale anonima contenente un metodo getType () che restituisce il tipo completamente parametrizzato.

3. Elenco degli oggetti polimorfici

3.1. Il problema

Considera un esempio di gerarchia di classi di animali:

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

Come serializziamo e deserializziamo List ? Potremmo usare TypeToken come abbiamo usato nella sezione precedente. Tuttavia, Gson non sarà ancora in grado di capire il tipo di dati concreto degli oggetti memorizzati nell'elenco.

3.2. Utilizzo del deserializzatore personalizzato

Un modo per risolvere questo problema consiste nell'aggiungere informazioni sul tipo al JSON serializzato. Rispettiamo le informazioni sul tipo durante la deserializzazione JSON. Per questo, dobbiamo scrivere il nostro serializzatore e deserializzatore personalizzati.

In primo luogo, introdurremo un nuovo campo String chiamato type nella classe base Animal . Memorizza il semplice nome della classe a cui appartiene.

Diamo un'occhiata alle nostre classi di esempio:

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 serializzazione continuerà a funzionare come prima senza problemi:

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

Per deserializzare l'elenco, dovremo fornire un deserializzatore personalizzato:

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

Qui, la mappa animalTypeRegistry mantiene la mappatura tra il nome della classe e il tipo di classe.

Durante la deserializzazione, estraiamo prima il campo del tipo appena aggiunto . Utilizzando questo valore, eseguiamo una ricerca sulla mappa animalTypeRegistry per ottenere il tipo di dati concreto. Questo tipo di dati viene quindi passato a fromJson () .

Vediamo come utilizzare il nostro deserializzatore personalizzato:

@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. Utilizzo di RuntimeTypeAdapterFactory

Un'alternativa alla scrittura di un deserializzatore personalizzato consiste nell'usare la classe RuntimeTypeAdapterFactory presente nel codice sorgente Gson. Tuttavia, non è esposto dalla libreria all'utente . Quindi, dovremo creare una copia della classe nel nostro progetto Java.

Fatto ciò, possiamo usarlo per deserializzare il nostro elenco:

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

Nota che il meccanismo sottostante è sempre lo stesso.

Abbiamo ancora bisogno di introdurre le informazioni sul tipo durante la serializzazione. Le informazioni sul tipo possono essere utilizzate successivamente durante la deserializzazione. Pertanto, il tipo di campo è ancora richiesto in ogni classe affinché questa soluzione funzioni. Semplicemente non dobbiamo scrivere il nostro deserializzatore.

RuntimeTypeAdapterFactory fornisce l'adattatore di tipo corretto in base al nome del campo passato ad esso e ai sottotipi registrati.

4. Conclusione

In questo articolo abbiamo visto come serializzare e deserializzare un elenco di oggetti usando Gson.

Come al solito, il codice è disponibile su GitHub.