Gson, da Java a JSON e viceversa: alcuni casi “difficili”

In questo secondo post dedicato a Gson vedremo alcuni casi un po’ più particolari rispetto quelli esposti nella . Esamineremo esclusivamente casi di deserializzazione: molto spesso, infatti, ci troviamo a consumare dei JSON prodotti da terze parti, e sui quali non abbiamo il controllo, che possono rivelare qualche “sorpresa”.

Quando un POJO è troppo

Può accadere, a volte, che il JSON da deserializzare sia molto verboso e magari a noi interessi solo una piccola parte all’interno. Se potessimo utilizzare solo l’approccio di passare un POJO a Gson, rischieremmo di perdere parecchio tempo a scrivere delle classi che alla fine sono solo dei semplici contenitori del dato che ci interessa davvero. Consideriamo, ad esempio, questo JSON:

{
   "lightObjectSet": {
      "objects": {
         "org": {
            "state": "Unchanged",
            "guid": "00000000-0000-0000-0000-000000000000",
            "id": "39",
            "name": "Test org",
            "shortName": "Test"
         }
      }
   }
}

L’unica cosa che ci interessa davvero è l’informazione contenuta nel nodo Org, perché scrivere un POJO anche per LightObjectSet e Objects? Per risolvere questo problema, Gson ci offre una classe che opera a “livello più basso” cioè la classe JsonParser.

Questa classe deserializza il nostro JSON e lo trasforma in una struttura ricorsiva ad albero dove ogni nodo è rappresentato da un JsonElement. A sua volta, JsonElement può estrarre uno dei tipi base di JSON cioè una mappa (JsonObject), un array (JsonArray) o un tipo primitivo. Inoltre, un JsonElement può essere utilizzato anche dalla classe Gson per deserializzare un altro POJO arbitrario. Partiamo con il codice: innanzitutto diamo la semplice definizione della classe Org.

public class Org {

    public String state;
        public String guid;
    public int id;
        public String name;
    public String shortName;
}

Al posto di usare direttamente la classe Gson, impieghiamo un JsonParser per ottenere il JSON deserializzato in un albero. L’albero è visitabile trasformando il nodo corrente, se ha figli, in una mappa o in un array. Attraversando in questo modo la struttura, si raggiunge il nodo di interesse e si passa il JsonElement corrente alla classe Gson, riportandosci a quanto già visto.

String json = "{ \"lightObjectSet\": { \"objects\": { \"org\": { \"state\":\"unchanged\", \"guid\":\"00000000-0000-0000-0000-000000000000\", \"id\":\"39\", \"name\":\"Test org\", \"shortName\":\"Test\" } } } }";

JsonElement je = new JsonParser().parse(json);
         
JsonObject root = je.getAsJsonObject();
JsonElement je2 = root.get("lightObjectSet");

JsonObject lightObjectSet = je2.getAsJsonObject();
JsonElement je3 = lightObjectSet.get("objects");

JsonObject objects = je3.getAsJsonObject();
JsonElement je4 = objects.get("org");

Gson g = new Gson();
Org org = g.fromJson(je4, Org.class);

Quando il server manda un po’ quello che vuole

L’utilizzo di JsonParser può essere utile anche quando il JSON da parserizzare non ha una struttura fissa, ma ad esempio, potrebbe variare in base alla richiesta fatta al server. In questo caso, possiamo utilizzare il parser per vedere cosa è contenuto nel JSON e decidere quale POJO utilizzare per la deserializzazione. Nell’esempio qui sotto il server invia due tipi completamente diversi di JSON.

JSON 1:

{"id" : "1", "name" : "David"}  //dati per la classe Person

JSON 2:

{"accountid" : "1188", "accountnumber" : "119295567"} // dati per la classe Account

Le risposte che arrivano possono corrispondere alla classe Person oppure alla classe Account, come fare per parserizzarle nell’oggetto giusto? E’ molto semplice, si da un “sbirciatina” con il parser al contenuto del JSON e poi si decide come procedere.

Gson g = new Gson();
JsonObject e = new JsonParser().parse(json).getAsJsonObject();

if (e.get("name") != null)
    return g.fromJson(json, Person.class);
if (e.get("accountid") != null)
    return g.fromJson(json, Account.class);

Quando non esiste il costruttore di default

Come visto nella prima parte, Gson lavora attraverso reflection: istanzia un oggetto della classe richiesta con il costruttore di default e poi riempie l’oggetto accedendo direttamente alle member variable. Tuttavia, può esserci il caso in cui la nostra classe di destinazione non contenga il costruttore di default ma solo un costruttore con parametri, come fare in questo caso? Si tratta la classe in maniera differente aggiungendo una strategia per deserializzare il JSON in maniera specifica.

Consideriamo la classe GeoPoint che viene creata passando due double come latitudine e longitudine;

public class GeoPoint {

   private double latitude;
   private double longitude;
   
   private GeoPoint(){}
   
   public GeoPoint(Double latitude, Double longitude){
       this.latitude = latitude;
       this.longitude = longitude;
   }
}

In questo caso l’approccio standard non funziona perché il costruttore di default è privato. Quando siamo in situazioni in cui la classe da deserializzare richiede una strategia specifica, Gson ci viene in aiuto attraverso l’interfaccia JsonDeserializer, che è dichiarata con il solo il metodo

 public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException;

dove abbiamo a disposizione:

  • un JsonElement che rappresenta il JSON che vogliamo deserializzare,
  • un Type che indica il tipo di oggetto che vogliamo ottenere e
  • un JsonDeserializationContext che possiamo pensare come l’istanza di Gson che sta chiamando il deserializzatore.

Il deserializzatore adatto al nostro caso corrisponde quindi a:

public class GeoPointDeserializer implements JsonDeserializer {

    @Override
    public GeoPoint deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException {
        if (json.isJsonNull())
            return null;
        
        Double x = json.getAsJsonObject().get("x").getAsDouble();
        Double y = json.getAsJsonObject().get("y").getAsDouble();
        return new GeoPoint(x, y);
    }
    
}

All’interno del metodo, stiamo semplicemente usando JsonElement come negli esempi visti prima. Estraiamo due valori dalla mappa, li passiamo al costruttore e restituiamo l’oggetto correttamente deserializzato.

Come possiamo aggiungere questa strategia a Gson? Utilizzando una classe di appoggio chiamata GsonBuilder che, come dice già il nome, implementa il per la classe Gson. In breve, è una classe con la quale si può parametrizzare la costruzione di un oggetto Gson tramite . Terminata la preparazione, si invoca un metodo di creazione (create) che ritorna un nuovo oggetto Gson con le proprietà richieste. Nel nostro caso usiamo il metodo di configurazione registerTypeAdapter che consente di associare al tipo GeoPoint un’istanza di GeoPointDeserializer.

Andiamo quindi a completare l’esempio usando la classe Polygon che contiene delle istanze di GeoPoint, un JSON da deserializzare e l’utilizzo di GsonBuilder.

La classe Polygon:

public class Polygon {

   public List points;

}

Il JSON corrispondente:

{
  "points": [
    {
      "x": 34.52788, 
      "y": -82.406371
    }, 
    {
      "x": 34.52855, 
      "y": -82.22676
    }, 
    {
      "x": 35.12927, 
      "y": -81.4931
    }
  ]
}

L’utilizzo del builder:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(GeoPoint.class, new GeoPointDeserializer());

Gson gson = gsonBuilder.create();

Polygon p = gson.fromJson(json, Polygon.class);

Quando il server manda quello che vuole, parte 2

Ora che abbiamo nella cassetta degli attrezzi anche JsonDeserializer, possiamo gestire il caso in cui il server invii un JSON che può cambiare nella struttura. Prendiamo il seguente esempio in cui a fronte della stessa richiesta vengono inviati i due seguenti JSON:

JSON 1:

{
    "firstName": "Mickey",
    "lastName": "Mouse",
    "address": {
        "street": "Walt Disney Street, 10",
        "postalCode": "12311"
    },
    "postalCode": "101101",
    "phoneNumber": "812123-1234"
}

JSON 2:

{
    "firstName": "Mickey",
    "lastName": "Mouse",
        "address": "Walt Disney Street, 10",
    "phoneNumbers": [
        "812 123-1234",
        "916 123-4567"
    ]
}

Il nodo Address può essere di tipo primitivo oppure aggregato, mentre phoneNumbers può essere espresso come semplice stringa (in questo caso si chiama è scritto al singolare) oppure come un array. Infine, per complicare un po’ le cose, potrebbero arrivare delle risposte che combinano i due casi precedenti. La strategia migliore per deserializzare questo tipo di JSON è quella di scrivere una classe che sia la più generale possibile e poi affidarsi ad un deserializzatore per gestire i nodi che cambiano. Andiamo quindi a definire una classe Card secondo questo principio.

 public class Card {
    
        public String firstName;
    public String lastName;
    public Address address;
    public List phoneNumbers;
          
    public static class Address {
         public String street;
         public String postalCode;
    }   
}       

Scriviamo il deserializzatore: come negli esempi precedenti, ci facciamo carico di estrarre le informazioni che ci interessano nodo per nodo e di istanziare tutto quello che serve.

public static class CardDeserializer implements JsonDeserializer {

      public Card deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

          if (json == null)
              return null;
          else {
              Card c = new Card();
              JsonObject jo = json.getAsJsonObject();
              c.firstName = jo.get("firstName").getAsString();
              c.lastName = jo.get("lastName").getAsString();
              JsonElement ja = jo.get("address");

              if (jo.get("address").isJsonObject()){
                 c.address  = context.deserialize(ja, Address.class);
              } else {
                 c.address = new Address();
                 c.address.street = jo.get("address").getAsString();
                 c.address.postalCode = jo.get("postalCode").getAsString();
              }

              JsonElement jsonPhoneNumbers = jo.get("phoneNumbers");
              if (jsonPhoneNumbers != null){
                 Type listType = new TypeToken>() {}.getType();
                 c.phoneNumbers = context.deserialize(jsonPhoneNumbers, listType);
              }

              JsonElement jsonPhoneNumber = jo.get("phoneNumber");
              if (jsonPhoneNumber != null){
                 c.phoneNumbers = new ArrayList<>(); //java7 here
                 c.phoneNumbers.add(jsonPhoneNumber.getAsString());
              }
              return c;
          }
      }
}

Da notare, a linea 14, il test che facciamo per verificare se l’indirizzo è primitivo o meno. Se siamo nel primo caso, instanziamo la classe Address e riempiamo l’unico campo che abbiamo a disposizione, altrimenti si fa uso del contesto corrente per deserializzare Address con il meccanismo di base. Alla stessa maniera, in base all’informazione che arriva (phoneNumber o phoneNumbers), creiamo un ArrayList per aggiungerci l’unico numero di telefono oppure riutilizziamo il contesto.

Conclusioni

Abbiamo visto come Gson ci permetta una configurazione precisa di come i JSON vengano convertiti in oggetti. Soprattutto nei casi di JSON polimorfici o particolarmente vasti, l’utilizzo dei metodi base di Gson non è abbastanza. Attraverso GsonBulder, è possibile specificare svariati parametri di deserializzazione e aggiungere dei deserializzatori (e serializzatori) specifici. All’interno di questi, l’utilizzo di JsonParser consente di esplorare il JSON a piacimento e risolvere un gran numero di casi.

Link per approfondire

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