Gson, da Java a JSON e viceversa: primi passi

La lista di librerie per rappresentare un oggetto Java in JSON non è sicuramente corta e conta ad oggi ben 24(!) progetti. Tra queste c’è Gson, una soluzione molto potente e flessibile realizzata da Google. In questa guida su Gson suddivisa in due parti, vedremo come utilizzare questa libreria partendo dai casi più semplici fino ad arrivare a quelli più complessi.

Ma quali caratteristiche ha Gson che la differenziano rispetto alle altre? Innanzitutto, questa libreria è semplice da utilizzare ma consente allo stesso tempo un controllo fine su come gli oggetti vengono trasformati in JSON. Inoltre, Gson può essere utilizzata anche su oggetti di cui non si dispone del sorgente, supporta l’utilizzo dei generics e lavora su oggetti arbitrariamente complessi.

Si accontenta di un POJO

Nel primo esempio vedremo come la forma più semplice di utilizzo che si può fare di Gson, preveda l’uso di un POJO (o di una qualsiasi classe Java). Gson trasforma in JSON il POJO e viceversa senza chiederci alcuno sforzo, basta istanziare l’oggetto Gson e chiamare i metodi fromJson e toJson.

public class Person{
   private String firstName;
   private String lastName;
   private int age;
   
   public Person(String firstName, String lastName, int age){
      this.firstName = firstName;
      this.lastName = lastName;
      this.age = age;  
   }

   @Override
   public String toString() {
      return "Person [firstName=" + firstName + ", lastName=" + lastName + ", age=" + age + "]";
   }
}
public class FirstExample {
  
   public static void main(String[] args){
      
      Person santa = new Person("Santa", "Claus", 1000);
      Gson gson = new Gson();
      System.out.println(g.toJson(santa));
      
      String json = "{\"firstName\":\"Peter\",\"worstEnemy\":\"Captain Hook\",\"age\":8}";
      Person peterPan = gson.fromJson(json, Person.class);
      
      System.out.println(peterPan);
   }
}

Il risultato è il seguente

{"firstName":"Santa","lastName":"Claus","age":1000}
Person [firstName=Peter, lastName=null, age=8]

Come si è visto, con una riga di codice si gestisce tranquillamente la conversione. Faccio notare due cose nel caso in cui non siano balzate all’occhio:

  • Durante la deserializazione, Gson non si fa molti problemi a scrivere nelle member variable dell’istanza anche se i setter non sono definiti. Come si diceva, Gson vuole consentire le trasformazioni in/da JSON anche su classi di cui non si dispone del sorgente, e per far questo utilizza la reflection. In definitiva, non aspettatevi che i vostri metodi vengano chiamati se li definite.

  • La relazione tra campi JSON e member variable è uno a uno, cioè vengono serializzate tutte le member variable dell’istanza mentre se un “campo” JSON non ha una corrispondente member variable (con esattamente lo stesso nome), il dato non viene deserializzato.

Mappare o escludere

Fare in modo che Gson utilizzi un nome diverso dal quello della member variable, oppure fargli escludere un campo durante la serializzazione è semplice e si fa tramite annotazioni. L’annotazione di campo @SerializedName permette di specificare un nome alternativo per il campo, mentre l’annotazione @Expose permette di specificare quali campi verranno serializzati. Tuttavia, spesso è più facile dire a Gson quali campi non si vogliono trattare: basta aggiungere l’attributo transient al campo.

public class Train {
   
   @SerializedName(value="number")
   private Integer _number;
   
   @SerializedName(value="departure")
   private String _fromCity;
   
   @SerializedName(value="arrival")
   private String _toCity;
   
   private transient String _fullDescription;
   
   public Train(Integer number, String from, String to){
      _number = number;
      _fromCity = from;
      _toCity = to;
   }
   
   public String toString(){
      if (_fullDescription == null)
         _fullDescription = number+" ["+_fromCity+", "+ _toCity+"]";
      return _fullDescription;
   }
}
public class SecondExample {

   public static void main(String[] args) {
      Train intercity = new Train(34111, "Venice", "Milan");
      Gson gson = new Gson();
      System.out.println(gson.toJson(intercity));
   }
}

il cui risultato è:

{"number":34111,"departure":"Venice","arrival":"Milan"}

Usare i generics

Anche usare i generics è molto semplice. Per fare un esempio, partiamo questa volta dal JSON e vediamo come deserializzarlo:

{
    "train": {
        "number": 9403, 
        "departure": "Rome", 
        "arrival": "Florence"
    }, 
    "persons": [
        {
            "firstName": "Billy", 
            "lastName": "The Kid", 
            "age": 30
        }, 
        {
            "firstName": "John", 
            "lastName": "Rambo", 
            "age": 27
        }
    ]
}

Questo JSON contiene due tipi oggetti che abbiamo già manipolato negli esempi precedenti, si tratta di deserializzarli assieme tenendo presente che c’è una collezione di Person da trattare. Per fare questo, introduciamo un oggetto “contenitore” che utilizza i generics e raggruppa Train e Person.

public class ThirdExample {
   
   public static class Container {
      
      public Train train;
      public List<Person> persons = new ArrayList<>();
      
      @Override
      public String toString() {
         return "Container [train=" + train + ", persons=" + persons + "]";
      }
      
   }
   
   public static void main(String[] args){
     
      String json = "{\"train\":{\"number\":9403,\"departure\":\"Rome\",\"arrival\":\"Florence\"},\"persons\":[{\"firstName\":\"Billy\",\"lastName\":\"The Kid\",\"age\":30},{\"firstName\":\"John\",\"lastName\":\"Rambo\",\"age\":27}]}";
      
      Gson gson = new Gson();   
      Container container = gson.fromJson(json, Container.class);
     
      System.out.println(container);
      
   }

}

Il risultato chiaramente è quello atteso:

Container [train=9403 [Rome, Florence], persons=[Person [firstName=Billy, lastName=The Kid, age=30], Person [firstName=John, lastName=Rambo, age=27]]]

Una piccola avvertenza sui generics

Nel caso in cui si voglia recuperare dal JSON direttamente una lista di generics, bisogna fare un passaggio aggiuntivo quando si specifica il tipo di classe che ti vuole ottenere dalla deserializzazione. L’esempio sottostante, cioè, non funziona.

package it.cosenonjaviste.gson;

import java.util.*;

import com.google.gson.Gson;

public class FourthExample {
   
   public static void main(String[] args){
      List<Person> aListOfPersons = new ArrayList<>();
      aListOfPersons.add(new Person("Huey", null, 5));
      aListOfPersons.add(new Person("Dewey", null, 5));
      aListOfPersons.add(new Person("Louie", null, 5));
      
      Gson gson = new Gson();
      String json = gson.toJson(aListOfPersons);
      
      List<Person> nephews = gson.fromJson(json, List.class);
      System.out.println(nephews);
      List<Person> nephews2 = gson.fromJson(json, aListOfPersons.getClass());
      System.out.println(nephews2);
      
   }

}
[{firstName=Huey, age=5.0}, {firstName=Dewey, age=5.0}, {firstName=Louie, age=5.0}]
[{firstName=Huey, age=5.0}, {firstName=Dewey, age=5.0}, {firstName=Louie, age=5.0}]

In effetti, Gson ha prodotto qualcosa, ma non si tratta di una lista di Person ma piuttosto di una lista di Map (notare il risultato di toString). Cosa è successo? Nel primo caso, abbiamo detto a Gson di utilizzare List ma senza specificare il tipo di contenuto della lista. Gson fa quindi l’unica cosa che può fare, deserializza un oggetto JSON che è rappresentato come mappa con.. una mappa!

Nel secondo caso invece, abbiamo passato direttamente la classe associata alla lista di partenza, ci si aspettava a questo punto che Gson utilizzasse una lista di Person, invece il risultato è sempre una mappa. Qui Gson non centra, è Java la causa del disatteso risultato. A runtime, Java non ha informazione sui generics per cui il metodo getClass torna sempre List come risultato, a prescindere dal tipo di lista che è stata dichiarata. Di conseguenza Gson non può sapere con cosa ha a che fare e istanzia nuovamente delle mappe. Fortunatamente esiste una piccola classe di utilità che permette di evitare il problema che si chiama TypeToken, andiamo quindi a riscrivere la deserializzazione in questo modo.

TypeToken<List<Person>> listType = new TypeToken<List<Person>>() {};
List<Person> nephews3 = gson.fromJson(json, listType.getType());

Conclusioni

Gson è una libreria molto potente, con letteralmente una riga di codice permette di trasformazioni JSON molto complesse. In questa prima parte abbiamo visto volutamente dei casi semplici, per dimostrare le potenzialità dello strumento. Spesso, però, i JSON che circolano in rete sono oggetti più complicati per dimensione oppure perché posso essere, diciamo così, “polimorfici”. Nella prossima puntata andremo quindi a esaminare casi più complessi presi da situazioni reali.

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Databiz Srl. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub