In questo secondo post dedicato a Gson vedremo alcuni casi un po’ più particolari rispetto quelli esposti nella prima parte. 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:

[code lang=”javascript”]
{
"lightObjectSet": {
"objects": {
"org": {
"state": "Unchanged",
"guid": "00000000-0000-0000-0000-000000000000",
"id": "39",
"name": "Test org",
"shortName": "Test"
}
}
}
}
[/code]

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.

[code lang=”java”]
public class Org {
public String state;
public String guid;
public int id;
public String name;
public String shortName;
}
[/code]

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.

[code lang=”java”]
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);
[/code]

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:
[code lang=”javascript”]
{"id" : "1", "name" : "David"} //dati per la classe Person
[/code]
JSON 2:
[code lang=”javascript”]
{"accountid" : "1188", "accountnumber" : "119295567"} // dati per la classe Account
[/code]

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.

[code lang=”java”]
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);
[/code]

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;

[code lang=”java”]
public class GeoPoint {
private double latitude;
private double longitude;
private GeoPoint(){}
public GeoPoint(Double latitude, Double longitude){
this.latitude = latitude;
this.longitude = longitude;
}
}
[/code]

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

[code lang=”java”]
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException;
[/code]
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:

[code lang=”java”]
public class GeoPointDeserializer implements JsonDeserializer<GeoPoint> {
@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);
}
}
[/code]

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 builder pattern per la classe Gson. In breve, è una classe con la quale si può parametrizzare la costruzione di un oggetto Gson tramite diversi metodi di configurazione. 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:

[code lang=”java”]
public class Polygon {
public List<GeoPoint> points;
}
[/code]

Il JSON corrispondente:

[code lang=”java”]
{
"points": [
{
"x": 34.52788,
"y": -82.406371
},
{
"x": 34.52855,
"y": -82.22676
},
{
"x": 35.12927,
"y": -81.4931
}
]
}
[/code]

L’utilizzo del builder:

[code lang=”java”]
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(GeoPoint.class, new GeoPointDeserializer());
Gson gson = gsonBuilder.create();
Polygon p = gson.fromJson(json, Polygon.class);
[/code]

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:
[code lang=”java”]
{
"firstName": "Mickey",
"lastName": "Mouse",
"address": {
"street": "Walt Disney Street, 10",
"postalCode": "12311"
},
"postalCode": "101101",
"phoneNumber": "812123-1234"
}
[/code]
JSON 2:
[code lang=”java”]
{
"firstName": "Mickey",
"lastName": "Mouse",
"address": "Walt Disney Street, 10",
"phoneNumbers": [
"812 123-1234",
"916 123-4567"
]
}
[/code]

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.
[code lang=”java”]
public class Card {
public String firstName;
public String lastName;
public Address address;
public List<String> phoneNumbers;
public static class Address {
public String street;
public String postalCode;
}
}
[/code]

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.

[code lang=”java”]
public static class CardDeserializer implements JsonDeserializer<Card> {
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<List<String>>() {}.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;
}
}
}
[/code]

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