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 Listpoints; }
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 ListphoneNumbers; 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
- Gson: deserialize internal part
- How to create more than one object with Gson and how can different objects explicit referenced in a Java class by using one JSON file?
- “Unparseable date: 1302828677828” trying to deserialize with Gson a millisecond-format date received from server
- Retrofit Gson serialize Date from JSON string into java.util.Date
- Deserialize a JSON that changes internal nodes