IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

 

Développons en Java   2.30  
Copyright (C) 1999-2022 Jean-Michel DOUDOUX    (date de publication : 15/06/2022)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

55. Gson

 

chapitre    5 5

 

Niveau : niveau 3 Intermédiaire 

 

 

Gson est une bibliothèque open source développée par Google pour convertir un objet Java dans sa représentation JSON et vice versa.

Le site officiel est à l'url : https://github.com/google/gson

Pour utiliser Gson dans une application, il faut ajouter le fichier gson-version.jar au classpath.

Pour utiliser Gson dans un projet Maven, il faut ajouter la dépendance :

Exemple :
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.2.4</version>
    </dependency>

Gson peut lire ou écrire des documents JSON en utilisant deux modèles de programmation :

  • object model : ce modèle est similaire à DOM pour XML
  • streaming : ce modèle est similaire au parser de type pull

Plusieurs versions ont été diffusées :

  • 1.1 : juillet 2008
  • 1.2 : août 2008, amélioration des performances, support pour des documents de plus de 15Mo
  • 1.2.3 : octobre 2008, support des maps, Gson est thread-safe
  • 1.3 : avril 2009, la méthode Gson.toJson() attend un paramètre de type Appendable, support de la désérialisation des valeurs double NaN et infinity, nouvelle API Parser, ajout de la méthode generateNonExecutableJson() à la classe GsonBuilder, support de FieldNamingStrategy
  • 1.4 : octobre 2009, support des stratégies d'exclusions, naming policy LOWER_CASE_WITH_DASHES
  • 1.5 : août 2010, naming policy UPPER_CAMEL_CASE_WITH_SPACES, amélioration des performances
  • 1.6 : octobre 2010, nouveau parser qui améliore les performances, API Streaming
  • 1.7 janvier 2011, amélioration des performances,
  • 2.0 : novembre 2011, plus rapide, null est sérialisé en "null",
  • 2.1 : décembre 2011
  • 2.2 : mai 2012
  • 2.2.2 : juillet 2012
  • 2.2.3 : avril 2013
  • 2.2.4 : mai 2013
  • 2.3 : oaût 2014
  • 2.3.1 : novembre 2014
  • 2.4 : octobre 2015
  • 2.5 : novembre 2015
  • 2.6 : février 2016
  • 2.7 : juin 2016
  • 2.8.0 : octobre 2016
  • 2.8.1 : mai 2017
  • 2.8.2 : septembre 2017
  • 2.8.3 : avril 2018
  • 2.8.4 : mai 2018
  • 2.8.5 : mai 2018

Ce chapitre contient plusieurs sections :

 

55.1. La classe Gson

La classe Gson est la classe principale de l'API Object Model.

Une instance de la classe Gson peut être obtenue :

  • en invoquant son constructeur pour obtenir une instance avec la configuration par défaut qui permet de facilement répondre aux cas simples
  • en utilisant une fabrique du type GsonBuilder qui permet de configurer précisément l'instance créée

Une instance de type Gson ne maintient aucun état lors de l'invocation de ses méthodes.

Ces deux méthodes principales possèdent plusieurs surcharges :

  • fromJson() : permet de créer un objet qui encapsule les données d'un document JSON. Ces surcharges attendent deux paramètres qui sont le document JSON (JsonElement, JsonReader, Reader, String) et le type d'objet à créer (Class ou Type)
  • toJson() : permet de créer une représentation JSON d'un objet (Objet) ou d'un élément (JsonElement)

 

55.2. La sérialisation

La sérialisation de données dans leur représentation JSON se fait en utilisant une des surcharges de la méthode toJson() de la classe Gson.

L'objet passé en paramètre de la méthode toJson() peut être une valeur d'un type commun (type primitif, wrapper, chaîne de caractères, tableaux).

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();

    System.out.println("1 -> " + gson.toJson(1));
    System.out.println("abcde -> " + gson.toJson("abcde"));
    System.out.println("Long(10) -> " + gson.toJson(new Long(10)));
    final int[] values = { 1, 2, 3 };
    System.out.println("{1,2,3} -> " + gson.toJson(values));
  }
}
Résultat :
1 -> 1
abcde -> "abcd"
Long(10) -> 10
{1,2,3} -> [1,2,3]

Il est possible de passer un tableau en paramètre de la méthode toJson().

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();

    final int[] entiers = { 1, 2, 3, 4 };
    System.out.println("entiers -> " + gson.toJson(entiers));
    final int[][] valeurs = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
    System.out.println("valeurs -> " + gson.toJson(valeurs));
    final String[] chaines = { "ab", "cd", "ef" };
    System.out.println("chaines -> " + gson.toJson(chaines));
  }
}
Résultat :
entiers -> [1,2,3,4]
valeurs -> [[1,2,3],[4,5,6],[7,8,9]]
chaines -> ["ab","cd","ef"]

Il est possible de passer une instance d'une classe qui encapsule les données à sérialiser en paramètre de la méthode toJson().

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.annotations.SerializedName;

public class Coordonnees {

  private final int abscisse;
  private final int ordonnee;

  public Coordonnees(final int abscisse, final int ordonnee) {
    super();
    this.abscisse = abscisse;
    this.ordonnee = ordonnee;
  }

  public int getAbscisse() {
    return this.abscisse;
  }

  public int getOrdonnee() {
    return this.ordonnee;
  }

  @Override
  public String toString() {
    return "Coordonnees [abscisse=" + this.abscisse + ", ordonnee=" + this.ordonnee + "]";
  }
}
Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();

    final Coordonnees coordonnees = new Coordonnees(120, 450);

    final String json = gson.toJson(coordonnees);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {"abscisse":120,"ordonnee":450}

Par défaut, tous les champs de la classe et des classes mères sont utilisés lors de la sérialisation même ceux déclarés private.

Tous les champs marqués avec le mot clé transient sont ignorés lors de la sérialisation. Les champs null sont aussi par défaut ignorés durant la sérialisation.

Les champs d'une classe interne qui font référence à la classe englobante sont ignorés (champ synthetic).

Attention : il ne faut pas sérialiser un objet qui contient une référence circulaire.

Il est possible de fournir une collection en paramètre de la méthode toJson().

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.ArrayList;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();

    final List<Integer> listeEntier = new ArrayList<Integer>();
    listeEntier.add(1);
    listeEntier.add(2);
    listeEntier.add(3);
    System.out.println("liste -> " + gson.toJson(listeEntier));

    final List listeObjet = new ArrayList();
    listeObjet.add("chaine");
    listeObjet.add(123);
    final Coordonnees coordonnees = new Coordonnees(120, 450);
    listeObjet.add(coordonnees);
    final String json = gson.toJson(listeObjet);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
liste -> [1,2,3]
Resultat = ["chaine",123,{"abscisse":120,"ordonnee":450}]

Il est possible de sérialiser une collection qui contient des objets de types différents mais il ne sera pas possible de les désérialiser car rien ne permet de préciser le type de chacune des valeurs.

Les collections de type Map sont sérialisées de manière particulière.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.HashMap;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();
    final Map<Integer, String> valeurs = new HashMap<Integer, String>();
    valeurs.put(1, "Valeur1");
    valeurs.put(2, "Valeur2");
    valeurs.put(3, "Valeur3");
    final String json = gson.toJson(valeurs);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {"2":"Valeur2","1":"Valeur1","3":"Valeur3"}

 

55.3. La désérialisation

La désérialisation de données à partir de leur représentation JSON se fait en utilisant une des surcharges de la méthode fromJson() de la classe Gson.

La méthode fromJson() possède plusieurs surcharges :

Méthode

Rôle

String toJson(JsonElement jsonElement)

Convertir un JsonElement dans sa représentation JSON

void toJson(JsonElement jsonElement, Appendable writer)

Ecrire la représentation JSON du JsonElement dans l'instance de type Appendable fournie en paramètre

void toJson(JsonElement jsonElement, JsonWriter writer)

Ecrire la représentation JSON du JsonElement dans l'instance de type Writer fournie en paramètre

String toJson(Object src)

Convertir un objet dans sa représentation JSON

void toJson(Object src, Appendable writer)

Ecrire la représentation JSON de l'objet dans l'instance de type Appendable fournie en paramètre

String toJson(Object src, Type typeOfSrc)

Convertir un objet typé avec un generic dans sa représentation JSON

void toJson(Object src, Type typeOfSrc, Appendable writer)

Ecrire la représentation JSON de l'objet typé avec un generic dans l'instance de type Appendable fournie en paramètre

void toJson(Object src, Type typeOfSrc, JsonWriter writer)

Ecrire la représentation JSON de l'objet typé avec un generic dans l'instance de type JsonWriter fournie en paramètre


Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.Arrays;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().setPrettyPrinting().create();

    final int unInt = gson.fromJson("1", int.class);
    System.out.println(unInt);
    final Integer unInteger = gson.fromJson("1", Integer.class);
    System.out.println(unInteger);
    final Long unLong = gson.fromJson("1", Long.class);
    System.out.println(unLong);
    final Boolean booleen = gson.fromJson("false", Boolean.class);
    System.out.println(booleen);
    final String chaine = gson.fromJson("\"abc\"", String.class);
    System.out.println(chaine);
    final String[] chaine2 = gson.fromJson("[\"abc\"]", String[].class);
    System.out.println(Arrays.deepToString(chaine2));
  }
}
Résultat :
1
1
1
false
abc
[abc]

Il est possible désérialiser la représentation JSON d'un objet en la passant en paramètre de la méthode fromJson() avec le type de la classe à produire.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();

    final String json = "{\"id\":1,\"nom\":\"nom1\",\"prenom\":\"prenom1\"}";
    final Personne personne = gson.fromJson(json, Personne.class);
    System.out.println(personne);
  }
}
Résultat :
Personne [id=1, nom=nom1, prenom=prenom1]

Lors de la désérialisation, les valeurs qui ne sont pas contenues dans le document JSON sont initialisées avec null.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();

    final String json = "{\"id\":1,\"nom\":\"nom1\"}";
    final Personne personne = gson.fromJson(json, Personne.class);
    System.out.println(personne);
  }
}
Résultat :
Personne [id=1, nom=nom1, prenom=null]

La classe doit posséder un constructeur par défaut. Le nom de la classe n'a pas d'importance, par contre la casse du nom des champs doit correspondre aux clés dans la représentation JSON. Gson utilise l'introspection pour alimenter les champs, donc il n'est pas obligatoire de disposer de setter dans la classe.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().setPrettyPrinting().create();

    final String json = "{\"ID\":1,\"NOM\":\"nom1\"}";
    final Personne personne = gson.fromJson(json, Personne.class);
    System.out.println(personne);
  }
}
Résultat :
Personne [id=0, nom=null, prenom=null]

Il est possible de désérialiser des données stockées dans un tableau.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.Arrays;

public class Groupe {

  private final String     nom;
  private final Personne[] personnes;
  private int              nbPersonnes = 0;

  public Groupe(final String nom) {
    super();
    this.nom = nom;
    this.personnes = new Personne[10];
  }

  public String getNom() {
    return this.nom;
  }

  public Personne[] getPersonnes() {
    return this.personnes;
  }

  public void ajouter(final Personne personne) {
    if (this.nbPersonnes < 10) {
      this.personnes[this.nbPersonnes] = personne;
      this.nbPersonnes++;
    }
  }

  @Override
  public String toString() {
    return "Groupe [nom=" + this.nom + ", personnes=" 
        + Arrays.toString(this.personnes) + ", nbPersonnes="
        + this.nbPersonnes + "]";
  }

}
Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();

    final String json = "{\"nom\":\"Groupe1\",\"personnes\":"
      +"[{\"id\":1,\"nom\":\"nom1\",\"prenom\":\"prenom1\"},"
      +"{\"id\":2,\"nom\":\"nom2\",\"prenom\":\"prenom2\"},"
      +"null,null,null,null,null,null,null,null],\"nbPersonnes\":2}";
    final Groupe groupe = gson.fromJson(json, Groupe.class);

    System.out.println(groupe);
  }
}
Résultat :
Groupe [nom=Groupe1, personnes=[Personne
[id=1, nom=nom1, prenom=prenom1], Personne [id=2, nom=nom2, prenom=prenom2],
null, null, null, null, null, null, null, null], nbPersonnes=2]

Il est aussi possible de désérialiser des collections. Gson permet de sérialiser une collection contenant des objets de type divers mais ne permet pas de désérialiser une telle collection car il n'est pas possible de préciser le type de chacun des éléments. Pour être désérialisée, une collection doit contenir des objets d'un même type précisé sous la forme d'un generic.

Exemple :
package fr.jmdoudoux.dej.gson;
      
import java.util.ArrayList;
import java.util.List;

public class Groupe {
  private final String         nom;
  private final List<Personne> personnes = new ArrayList();

  public Groupe(final String nom) {
    super();
    this.nom = nom;
  }

  public String getNom() {
    return this.nom;
  }

  public List<Personne> getPersonnes() {
    return this.personnes;
  }

  public void ajouter(final Personne personne) {
    this.personnes.add(personne);
  }

  @Override
  public String toString() {
    return "Groupe [nom=" + this.nom + ", personnes=" + this.personnes  + "]";
  }
}
Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserialisation {
  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final String json = "{\"nom\":\"Groupe1\",\"personnes\":[{\"id\":1,\"nom\":\"nom1\","
      + "\"prenom\":\"prenom1\"},{\"id\":2,\"nom\":\"nom2\",\"prenom\":\"prenom2\"}]}";
    final Groupe groupe = gson.fromJson(json, Groupe.class);
    System.out.println(groupe);
  }
}
Résultat :
Groupe [nom=Groupe1, personnes=[Personne [id=1, nom=nom1,
prenom=prenom1], Personne [id=2, nom=nom2, prenom=prenom2]]]

 

55.4. La personnalisation de la sérialisation/désérialisation

Gson propose plusieurs fonctionnalités pour personnaliser les opérations de sérialisation et de désérialisation.

 

55.4.1. La classe GsonBuilder

La classe GsonBuilder est une fabrique qui permet de créer une instance de type Gson qui soit configurable.

Cette classe met en oeuvre le motif de conception Builder : chaque méthode qui permet de configurer une fonctionnalité de l'instance à créer renvoie l'instance du builder elle-même, ce qui permet de chaîner les invocations. La méthode create() permet d'obtenir l'instance.

La classe GsonBuilder propose plusieurs méthodes qui permettent de configurer les instances qui seront créées :

Méthode

Rôle

GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strategy)

Ajouter une stratégie d'exclusion des éléments lors de la désérialisation

GsonBuilder addSerializationExclusionStrategy(ExclusionStrategy strategy)

Ajouter une stratégie d'exclusion des éléments lors de la sérialisation

Gson create()

Créer une instance avec la configuration courante

GsonBuilder disableHtmlEscaping()

Désactiver l'échappement des caractères HTML (activé par défaut)

GsonBuilder disableInnerClassSerialization()

Désactiver la sérialisation des classes internes

GsonBuilder enableComplexMapKeySerialization()

 

GsonBuilder excludeFieldsWithModifiers(int... modifiers)

Exclure les champs qui possèdent le ou les modificateurs fournis en paramètres

GsonBuilder excludeFieldsWithoutExposeAnnotation()

Exclure tous les champs qui ne sont pas annotés avec @Expose et prendre en compte ceux qui le sont selon les paramètres de l'annotation

GsonBuilder generateNonExecutableJson()

Rendre le document JSON généré non exécutable par JavaScript en le préfixant par un texte

GsonBuilder registerTypeAdapter(Type type, Object typeAdapter)

 

GsonBuilder registerTypeAdapterFactory(TypeAdapterFactory factory)

 

GsonBuilder registerTypeHierarchyAdapter(Class<?> baseType, Object typeAdapter)

 

GsonBuilder serializeNulls()

Demander à Gson de sérialiser les champs null

GsonBuilder serializeSpecialFloatingPointValues()

Demander à Gson de sérialiser les valeurs spéciales pour les champs de type double (NaN, Infinity, -Infinity).

GsonBuilder setDateFormat(int style)

Préciser le style de format des dates

GsonBuilder setDateFormat(int dateStyle, int timeStyle)

Préciser le style de format des dates et des heures

GsonBuilder setDateFormat(String pattern)

Préciser le format des dates

GsonBuilder setExclusionStrategies(ExclusionStrategy... strategies)

Configurer Gson pour appliquer des stratégies d'exclusion pendant les sérialisations et les désérialisations

GsonBuilder setFieldNamingPolicy(FieldNamingPolicy namingConvention)

Configurer Gson pour appliquer une politique de nommage des champs

GsonBuilder setFieldNamingStrategy(FieldNamingStrategy fieldNamingStrategy)

Configurer Gson pour appliquer une stratégie de nommage de champs lors des sérialisations et des déserializations

GsonBuilder setLongSerializationPolicy(LongSerializationPolicy serializationPolicy)

Configurer Gson pour utiliser une stratégie de sérialisation des données de type long ou Long

GsonBuilder setPrettyPrinting()

Configurer Gson pour formater les documents générés

GsonBuilder setVersion(double ignoreVersionsAfter)

Préciser le numéro de version courant et ainsi activer l'utilisation des annotations @Since et @Until


L'ordre d'invocation des méthodes de configuration n'a pas d'importance.

Exemple :
 Gson gson = new GsonBuilder()
     .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
     .setDateFormat(DateFormat.LONG)
     .serializeNulls()
     .setPrettyPrinting()
     .setVersion(1.2)
     .create();

 

55.4.2. L'utilisation de Serializer et TypeAdapter

La sérialisation et désérialisation par défaut ne répond pas toujours aux besoins. Gson permet d'enregistrer et d'utiliser des classes qui permettront de réaliser une sérialisation ou une désérialisation personnalisée.

Gson définit deux interfaces qu'il faut implémenter selon les besoins :

  • JsonSerializer<T>
  • JsonDeserializer<T>

Les Serializer et les Deserializer doivent être enregistrés auprès d'une instance de type GsonBuilder en invoquant la méthode registerTypeAdapter(). Elle attend en paramètre la classe de l'instance à sérialiser et une instance du type de l'implémentation du Serializer.

Si les Serializer ne proposent pas de constructeur par défaut, il est nécessaire de proposer une implémentation de l'interface InstanceCreator<T>. Celle-ci doit aussi être enregistrée dans l'instance de type GsonBuilder en invoquant la méthode registerTypeAdapter(). Elle ne définit qu'une méthode createInstance() qui attend en paramètre le type de l'instance à créer.

Les exemples de cette section vont développer une personnalisation de la sérialisation/désérialisation pour une classe qui encapsule une chaîne de caractères.

Exemple :
package fr.jmdoudoux.dej.gson;

public class MaChaine {

  private String valeur;

  public String getValeur() {
    return this.valeur;
  }

  public void setValeur(final String valeur) {
    this.valeur = valeur;
  }

  @Override
  public String toString() {
    return "MaChaine [valeur=" + this.valeur + "]";
  }
}

 

55.4.2.1. L'interface JsonSerializer

L'interface JsonSerializer<T> définit les fonctionnalités d'un Serialiser personnalisé. Le type T permet d'indiquer le type de la classe à sérialiser.

Un Serializer permet de définir précisément comment une classe est sérialisée : ceci est particulièrement utile lorsque le mécanisme de sérialisation par défaut de Gson ne répond pas au besoin.

L'interface JsonSerializer ne définit qu'une seule méthode :

Méthode

Rôle

JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)

Méthode invoquée par Gson lorsqu'il faut sérialiser un objet de type T


Il faut définir une classe qui implémente l'interface JsonSerializer et redéfinir la méthode serialize(). Celle-ci renvoie une instance de type JsonElement qui encapsule l'arborescence des éléments du document JSON.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.lang.reflect.Type;

import org.apache.commons.codec.binary.Base64;

import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class MaChaineSerializer implements JsonSerializer<MaChaine> {
  @Override
  public JsonElement serialize(final MaChaine maChaine, 
    final Type typeOfSrc, final JsonSerializationContext context) {
    JsonElement resultat = null;

    if (maChaine == null) {
      resultat = JsonNull.INSTANCE;
    } else {
      if (maChaine.getValeur() == null) {
        resultat = JsonNull.INSTANCE;
      } else {
        resultat = new JsonPrimitive(new String(
          Base64.encodeBase64(maChaine.getValeur().getBytes())));
      }
    }
    return resultat;
  }
}

Il est nécessaire d'enregistrer un Serializer en invoquant la méthode registerTypeAdapter() de la classe GsonBuilder

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerializer {

  public static void main(final String[] args) {
    MaChaine maChaine = new MaChaine();
    maChaine.setValeur("ma valeur");

    Gson gson = new GsonBuilder().create();
    String json = gson.toJson(maChaine);
    System.out.println("Serialisation sans serializer = " + json);

    gson = new GsonBuilder().registerTypeAdapter(MaChaine.class, 
      new MaChaineSerializer()).create();
    json = gson.toJson(maChaine);
    System.out.println("Serialisation avec serializer = " + json);

    maChaine = new MaChaine();
    json = gson.toJson(maChaine);
    System.out.println("Serialisation null avec serializer = " + json);
  }
}
Résultat :
Serialisation sans serializer = {"valeur":"ma valeur"}
Serialisation avec serializer = "bWEgdmFsZXVy"
Serialisation null avec serializer = null

 

55.4.2.2. L'interface JsonDeserializer

L'interface JsonDeserializer<T> définit les fonctionnalités d'un Deserialiser personnalisé. Le type T permet d'indiquer le type de la classe à désérialiser.

Un Deserializer permet de définir précisément comment une classe est désérialisée : ceci est particulièrement utile lorsque le mécanisme de désérialisation par défaut de Gson ne répond pas au besoin.

L'interface JsonDeserializer ne définit qu'une seule méthode :

Méthode

Rôle

T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)

Méthode invoquée par Gson lorsqu'il faut désérialiser un objet de type T


Il faut définir une classe qui implémente l'interface JsonDeserializer et redéfinir la méthode deserialize(). Celle-ci renvoie une instance de type T encapsulant le résultat de la désérialisation du JsonElement qui encapsule l'arborescence des éléments du fragment JSON.

Exemple :
package fr.jmdoudoux.dej.gson;

 import java.lang.reflect.Type;

 import org.apache.commons.codec.binary.Base64;

 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonNull;
 import com.google.gson.JsonParseException;

 public class MaChaineDeserializer implements JsonDeserializer<MaChaine> {
   @Override
   public MaChaine deserialize(final JsonElement json, final Type typeOfT, 
       final JsonDeserializationContext context) throws JsonParseException {
     final MaChaine resultat = new MaChaine();

     if (json != JsonNull.INSTANCE) {
       final String valeurBase64 = json.getAsJsonPrimitive().getAsString();
       final String valeur = new String(Base64.decodeBase64(valeurBase64));
       resultat.setValeur(valeur);
     }
     return resultat;
   }
 }

Il est nécessaire d'enregistrer un Deserializer en invoquant la méthode registerTypeAdapter() de la classe GsonBuilder

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestDeserializer {

  public static void main(final String[] args) {
    MaChaine maChaine = new MaChaine();
    maChaine.setValeur("ma valeur");

    Gson gson = new GsonBuilder().create();
    String json = gson.toJson(maChaine);
    System.out.println("Serialisation sans serializer = " + json);

    gson = new GsonBuilder()
        .registerTypeAdapter(MaChaine.class, new MaChaineSerializer())
        .registerTypeAdapter(MaChaine.class, new MaChaineDeserializer())
        .create();
    json = gson.toJson(maChaine);
    System.out.println("Serialisation avec serializer = " + json);

    final MaChaine maChaineDes = gson.fromJson(json, MaChaine.class);
    System.out.println("Deserialisation avec serializer = " + maChaineDes);

    maChaine = new MaChaine();
    json = gson.toJson(maChaine);
    System.out.println("Serialisation null avec serializer = " + json);
  }
}
Résultat :
Serialisation sans serializer = {"valeur":"ma valeur"}
Serialisation avec serializer = "bWEgdmFsZXVy"
Deserialisation avec serializer = MaChaine [valeur=ma valeur]
Serialisation null avec serializer = null

 

55.4.2.3. La classe TypeAdapter

Depuis la version 2.1 de Gson, il est préférable d'utiliser une instance de type com.google.gson.TypeAdapter car elle utilise l'API de streaming qui est plus efficace.

La classe TypeAdapter possède plusieurs méthodes :

Méthode

Rôle

T fromJson(Reader in)

Convertir le document Json passé en paramètre en un objet Java de type T

T fromJson(String json)

Convertir le document Json passé en paramètre en un objet Java de type T

T fromJsonTree(JsonElement jsonTree)

Convertir le document Json passé en paramètre en un objet Java de type T

TypeAdapter <T> nullSafe()

 

Abstract T read(JsonReader in)

Lire un élément JSON (valeur, objet ou tableau) et le convertir en objet Java

String toJson(T value)

Convertir un objet en document JSON

void toJson(Writer out, T, value)

Convertir un objet en document JSON et l'envoyer dans le flux fourni en paramètre

JsonElement toJsonTree(T value)

Convertir un objet en un arbre d'éléments Json

abstract void write(JsonWriter out, T value)

Convertir un objet en un élément JSON (valeur, objet ou tableau)


Il faut définir une classe fille qui hérite de la classe TypeAdapter et redéfinir les méthodes abstraites read() et write().

Il est nécessaire d'enregistrer un adapter auprès de l'instance de type Gson en invoquant la méthode registerTypeAdapter de la classe GsonBuilder.

Dans l'exemple ci-dessous, l'adapter va sérialiser l'objet uniquement en encodant sa valeur en base 64 et vice versa lors de la désérialisation.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;

import org.apache.commons.codec.binary.Base64;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

public class MaChaineAdapter extends TypeAdapter<MaChaine> {

  @Override
  public MaChaine read(final JsonReader reader) throws IOException {
    final MaChaine resultat = new MaChaine();
    if (reader.peek() == JsonToken.NULL) {
      reader.nextNull();
    } else {
      final String valeurBase64 = reader.nextString();
      final String valeur = new String(Base64.decodeBase64(valeurBase64));
      resultat.setValeur(valeur);
    }
    return resultat;
  }

  @Override
  public void write(final JsonWriter writer, final MaChaine maChaine) throws IOException {
    if (maChaine == null) {
      writer.nullValue();
    } else {
      if (maChaine.getValeur() == null) {
        writer.nullValue();
      } else {
        writer.value(new String(Base64.encodeBase64(maChaine.getValeur().getBytes())));
      }
    }
  }
}
Résultat :
Serialisation sans adapter = {"valeur":"ma valeur"}
Serialisation avec adapter = "bWEgdmFsZXVy"
Deserialisation avec adapter = MaChaine [valeur=ma valeur]
Serialisation null avec adapter = null
Deserialisation null avec adapter = MaChaine [valeur=null]

 

55.4.3. L'interface InstanceCreator

Lors d'une opération de désérialisation, Gson doit pouvoir créer une instance de chaque classe dont il va avoir besoin. Ces classes devraient avoir un constructeur sans argument public ou private.

Avant la version 1.7, lorsqu'une classe qui devait être instanciée par Gson ne possèdaient pas de constructeur par défaut, il fallait définir une classe de type InstanceCreator<T>

Exemple :
package fr.jmdoudoux.dej.gson;

import java.lang.reflect.Type;

import com.google.gson.InstanceCreator;

public class MonChampInstanceCreator implements InstanceCreator<MonChamp<?>> {
  @SuppressWarnings("rawtypes")
  @Override
  public MonChamp createInstance(final Type type) {
    return new MonChamp(123);
  }
}

Une instance de type InstanceCreator devait être enregistrée dans le GsonBuilder en utilisant la méthode MonChampInstanceCreator.

Exemple :
    final GsonBuilder builder = new GsonBuilder();
    builder.registerTypeAdapter(MonChamp.class, new MonChampInstanceCreator()).create();

 

55.4.4. Le formatage de la représentation JSON

Par défaut, la représentation JSON faite par Gson est compacte : elle ne contient aucun élément de formatage.

Il est possible de configurer l'instance de type Gson créée en utilisant un GsonBuilder pour qu'elle applique un formatage de la représentation JSON en invoquant la méthode setPrettyPrinting().

Exemple :
package fr.jmdoudoux.dej.gson;
      
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {
  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().setPrettyPrinting().create();
    final Personne personne = new Personne(1, "nom1", "prenom1");
    final String json = gson.toJson(personne);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {
  "id": 1,
  "nom": "nom1",
  "prenom": "prenom1"
}

Par défaut, la classe Gson utilise la classe JsonCompactFormatter. Elle utilise la classe JsonPrintFormatter si le formatage a été demandé dans sa configuration.

 

55.4.5. Le support des valeurs null

Par défaut, les objets null sont ignorés par Gson, ce qui permet d'avoir des représentations JSON plus compacte.

Exemple :
package fr.jmdoudoux.dej.gson;
      
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {
  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final Personne personne = new Personne(1, "nom1", null);
    final String json = gson.toJson(personne);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat =
{"id":1,"nom":"nom1"}

Il est possible de configurer l'instance de type Gson créée en utilisant un GsonBuilder pour qu'elle tienne compte des objets null en invoquant la méthode serializeNulls().

Exemple :
package fr.jmdoudoux.dej.gson;
      
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {
  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().serializeNulls().create();
    final Personne personne = new Personne(1, "nom1", null);
    final String json = gson.toJson(personne);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat =
{"id":1,"nom":"nom1","prenom":null}

 

55.4.6. L'exclusion de champs

Gson propose plusieurs solutions pour exclure certains champs des opérations de sérialisation. Si ces solutions ne sont pas suffisantes, il est toujours possible de créer ses propres Serializer et Deserializer.

 

55.4.6.1. L'exclusion de champs sur la base de modificateurs

Par défaut, les champs qui sont définis avec le mot clé transient ou static sont ignorés par Gson.

Il est possible de configurer l'instance de type Gson pour qu'elle ignore les champs possédant certains modificateurs en invoquant la méthode excludeFieldsWithModifier() de la classe GsonBuilder. L'invocation de la méthode excludeFieldsWithModifiers() permet de modifier le comportement par défaut.

Exemple :
Gson gson = gsonBuilder.excludeFieldsWithModifiers(Modifier.STATIC,
  Modifier.TRANSIENT, Modifier.VOLATILE).create();

Il est possible d'utiliser toutes les constantes définies dans la classe java.lang.reflect.Modifier en paramètre de la méthode excludeFieldsWithModifier().

 

55.4.6.2. Les stratégies d'exclusion personnalisées

Pour des besoins plus particuliers, il est possible de définir sa propre stratégie d'exclusion et de la faire appliquer à l'instance de type Gson. Cette stratégie permet de décider si une classe ou un champ doit être pris en compte lors des opérations de Gson.

Il faut définir une classe qui implémente l'interface ExclusionStrategy. Cette interface définit deux méthodes :

Méthode

Rôle

boolean shouldSkipClass(Class<?> clazz)

Renvoyer un booléen qui précise si la classe fournie en paramètre doit être ignorée

boolean shouldSkipField(FieldAttributes f)

Renvoyer un booléen qui précise si le champ fourni en paramètre doit être ignoré


L'exemple ci-dessous va définir une annotation qui est un simple marqueur et qui sera utilisée par une stratégie d'exclusion personnalisée pour ignorer les champs marqués avec celle-ci.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
public @interface ExclureDeGson {
}

Il faut utiliser l'annotation sur les champs ou les classes concernées

Exemple :
package fr.jmdoudoux.dej.gson;

public class MonBean {

  @ExclureDeGson
  private String champ10;
  private String champ11;
  private String champ12;

  public MonBean() {
    super();
  }

  // ...

}

La stratégie personnalisée va ignorer toutes les classes et les champs annotés avec l'annotation ExclureDeGson.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;

public class MonExclusionStrategy implements ExclusionStrategy {

  public MonExclusionStrategy() {
  }

  @Override
  public boolean shouldSkipClass(final Class<?> clazz) {
    return clazz.getAnnotation(ExclureDeGson.class) != null;
  }

  @Override
  public boolean shouldSkipField(final FieldAttributes f) {
    return f.getAnnotation(ExclureDeGson.class) != null;
  }
}

Pour demander à Gson d'utiliser la stratégie d'exclusion, il faut en passer une instance en paramètre de la méthode setExclusionsStrategies() de la classe GsonBuilder.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean();
    monBean.setChamp10("champ10");
    monBean.setChamp11("champ11");
    monBean.setChamp12("champ12");

    final GsonBuilder builder = new GsonBuilder();
    builder.setExclusionStrategies(new MonExclusionStrategy());
    final Gson gson = builder.create();
    final String json = gson.toJson(monBean);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {"champ11":"champ11","champ12":"champ12"}

 

55.4.7. Le nommage des éléments

Il est possible de configurer l'instance de type Gson créée en utilisant un GsonBuilder pour qu'elle applique une règle de formatage lors du nommage d'un élément pour sa représentation JSON en invoquant sa méthode setFieldNamingPolicy().

L'énumération FieldNamingPolicy définit plusieurs valeurs qui correspondent à des conventions de nommage classiques :

Valeur

Rôle

IDENTITY

La politique de nommage est de reprendre le nom du champ tel quel

LOWER_CASE_WITH_DASHES

La politique de nommage est de tout mettre en minuscule, chaque mot séparé par un caractère tiret

LOWER_CASE_WITH_UNDERSCORES

La politique de nommage est de tout mettre en minuscule, chaque mot séparé par un caractère souligné

UPPER_CAMEL_CASE

La politique de nommage est de mettre la première lettre en majuscule

UPPER_CAMEL_CASE_WITH_SPACES

La politique de nommage est de mettre la première lettre de chaque mot en majuscule en séparant les mots par des espaces


Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final Personne personne = new Personne(1, "nom1", "prenom1");
    Gson gson = new GsonBuilder()
      .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
    String json = gson.toJson(personne);
    System.out.println("Resultat = " + json);

    gson = new GsonBuilder()
      .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES).create();
    json = gson.toJson(personne);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {"Id":1,"Nom":"nom1","Prenom":"prenom1"}
Resultat = {"id":1,"nom":"nom1","prenom":"prenom1"}

Il est aussi possible de définir sa propre stratégie de nommage en créant une classe qui implémente l'interface FieldNamingStrategy. Elle ne définit qu'une seule méthode :

Méthode

Rôle

String translateName (Field field)

Renvoyer le nom du champ JSON pour le champ de la classe fournie en paramètre


Exemple :
package fr.jmdoudoux.dej.gson;

import java.lang.reflect.Field;

import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean();
    monBean.setChamp10("champ10");
    monBean.setChamp11("champ11");
    monBean.setChamp12("champ12");

    final GsonBuilder builder = new GsonBuilder();
    builder.setFieldNamingStrategy(new FieldNamingStrategy() {
      @Override
      public String translateName(final Field field) {
        return "monapp_" + field.getName().toLowerCase();
      }
    });

    final Gson gson = builder.create();
    final String json = gson.toJson(monBean);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {"monapp_champ10":"champ10","monapp_champ11":"champ11","monapp_champ12":"champ12"}

Cette fonctionnalité permet de personnaliser le nom des champs dans le document JSON. Ceci peut être particulièrement utile lorsque le document JSON contient des noms de champs qui ne correspondent pas à des noms de variables légales en Java. C'est la cas, par exemple, si le nom du champ contient un caractère '-'.

L'API propose aussi la possibilité de forcer le nom d'un élément en utilisant l'annotation @SerializedName (cette possibilité est détaillée dans une des sections suivantes).

 

55.5. Les annotations de Gson

Gson propose plusieurs annotations pour faciliter la configuration des opérations de sérialisation/désérialisation :

  • @Expose : permet de préciser si un champ doit être utilisé ou non lors des opérations de sérialisation et de désérialisation
  • @SerializedName : permet de préciser le nom du champ qui sera utilisé lors des opérations de sérialisation/désérialisation
  • @Since : permet de définir à partir de quelle version le champ ou la classe doit être pris en compte
  • @Until : permet de définir jusqu'à quelle version le champ ou la classe doit être pris en compte

 

55.5.1. La personnalisation forcée des noms des champs

L'annotation @SerializedName permet de préciser le nom du champ qui sera utilisé lors des opérations de sérialisation/désérialisation. Le nom est précisé comme valeur de l'annotation : il doit respecter les règles applicables à un nom de champs JSON.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerializedName {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();

    Coordonnees coordonnees = new Coordonnees(120, 450);

    final String json = gson.toJson(coordonnees);
    System.out.println("Resutlat = " + json);

    coordonnees = gson.fromJson("{\"abscisse\":120,\"ordonnee\":450}", Coordonnees.class);
    System.out.println(coordonnees);

    coordonnees = gson.fromJson("{\"x\":120,\"y\":450}", Coordonnees.class);
    System.out.println(coordonnees);
  }
} 
Résultat :
Resutlat = {"x":120,"y":450}
Coordonnees [abscisse=0, ordonnee=0]
Coordonnees [abscisse=120, ordonnee=450] 

La sérialisation et la désérialisation de l'objet utilisent les noms précisés par l'annotation.

L'utilisation de cette annotation ne nécessite aucune configuration particulière. Elle outrepasse n'importe quel FieldNamingPolicy qui pourrait être précisé dans la configuration.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerializedName {

  public static void main(final String[] args) {
    final GsonBuilder builder = new GsonBuilder();
    builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE);
    final Gson gson = builder.create();

    final Coordonnees coordonnees = new Coordonnees(120, 450);

    final String json = gson.toJson(coordonnees);
    System.out.println("Resultat  = " + json);
  }
}
Résultat :
Resultat  = {"x":120,"y":450}

 

55.5.2. La désignation des champs à prendre en compte

En plus de l'utilisation du mot clé transient qui permet d'exclure complètement un champ lors des opérations de Gson et d'utiliser des classes filles de type JsonSerializer ou JsonDeserializer pour avoir un contrôle très fin sur ses opérations, Gson propose l'annotation @Expose. Elle ne s'utilise que sur des champs.

L'annotation @Expose possède deux attributs optionnels :

Attribut

Rôle

deserialize

Booléen qui précise si le champ doit être utilisé lors des opérations de désérialisation

serialize

Booléen qui précise si le champ doit être utilisé lors des opérations de sérialisation


Selon les valeurs fournies à ces deux attributs, le champ concerné sera pris en compte lors des opérations réalisées par Gson. Leur valeur par défaut est true.

Exemple d'utilisation

Sérialisation

Desérialisation

private String monChamp;

Non

Non

@Expose
private String monChamp;

Oui

Oui

@Expose(deserialize = false)
private String monChamp;

Oui

Non

@Expose(serialize = false)
private String monChamp;

Non

Oui

@Expose(serialize = false, deserialize = false)
private String monChamp;

Non

Non


Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.annotations.Expose;

public class MonObjet {

  private final String champ1;
  @Expose(serialize = false, deserialize = false)
  private final String champ2;
  @Expose(serialize = false)
  private final String champ3;
  @Expose(deserialize = false)
  private final String champ4;

  public MonObjet(final String champ1, final String champ2, 
    final String champ3, final String champ4) {
    super();
    this.champ1 = champ1;
    this.champ2 = champ2;
    this.champ3 = champ3;
    this.champ4 = champ4;
  }

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public String getChamp3() {
    return this.champ3;
  }

  public String getChamp4() {
    return this.champ4;
  }

  @Override
  public String toString() {
    return "MonObjet [champ1=" + this.champ1 + ", champ2=" + this.champ2 + ", 
      champ3=" + this.champ3 + ", champ4=" + this.champ4 + "]";
  }
}

Pour permettre la prise en compte de l'annotation @Expose, il est nécessaire de configurer l'instance de type Gson : pour cela, il faut invoquer la méthode excludeFieldsWithoutExposeAnnotation() de la classe GsonBuilder.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final MonObjet monObjet = new MonObjet("valeur1", "valeur2", "valeur3", "valeur4");

    final GsonBuilder builder = new GsonBuilder();
    Gson gson = builder.create();
    String json = gson.toJson(monObjet);
    System.out.println("Resultat = " + json);

    builder.excludeFieldsWithoutExposeAnnotation();
    gson = builder.create();
    json = gson.toJson(monObjet);
    System.out.println("Resultat = " + json);
  }
}
Résultat :
Resultat = {"champ1":"valeur1","champ2":"valeur2","champ3":"valeur3","champ4":"valeur4"}
Resultat = {"champ4":"valeur4"}

L'utilisation de l'annotation @Expose(serialize = false, deserialize = false) est équivalente à déclarer le champ avec le modificateur transient.

 

55.5.3. La gestion des versions

GSON propose deux annotations utilisables sur des classes ou des champs qui permettent de déterminer ceux qui doivent être sérialisés et désérialisés en fonction d'un numéro de version :

  • @Since : permet de définir à partir de quelle version le champ ou la classe doit être pris en compte
  • @Until (à partir de la version 1.3) : permet de définir jusqu'à quelle version le champ ou la classe doit être pris en compte
Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;

public class MonBean {

  @Until(1.1)
  private String champ10;
  private String champ11;
  @Since(1.2)
  private String champ12;

  public MonBean() {
    super();
  }

  public String getChamp10() {
    return this.champ10;
  }

  public void setChamp10(final String champ10) {
    this.champ10 = champ10;
  }

  public String getChamp11() {
    return this.champ11;
  }

  public void setChamp11(final String champ11) {
    this.champ11 = champ11;
  }

  public String getChamp12() {
    return this.champ12;
  }

  public void setChamp12(final String champ12) {
    this.champ12 = champ12;
  }
}

Le numéro de version courant doit être précisé à l'instance de type Gson en invoquant la méthode setVersion() de la classe GsonBuilder. Elle attend en paramètre la valeur de la version courante.

Si la version courante n'est pas précisée à l'instance de type Gson, alors les annotations @Since et @Until sont simplement ignorées.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean();
    monBean.setChamp10("champ10");
    monBean.setChamp11("champ11");
    monBean.setChamp12("champ12");

    final GsonBuilder builder = new GsonBuilder();
    Gson gson = builder.create();
    String json = gson.toJson(monBean);
    System.out.println("Resultat = " + json);

    builder.setVersion(1.0);
    gson = builder.create();
    json = gson.toJson(monBean);
    System.out.println("Resultat version 1.0 = " + json);

    builder.setVersion(1.1);
    gson = builder.create();
    json = gson.toJson(monBean);
    System.out.println("Resultat version 1.1 = " + json);

    builder.setVersion(1.2);
    gson = builder.create();
    json = gson.toJson(monBean);
    System.out.println("Resultat version 1.2 = " + json);
  }
}
Résultat :
Resultat = {"champ10":"champ10","champ11":"champ11","champ12":"champ12"}
Resultat version 1.0 = {"champ10":"champ10","champ11":"champ11"}
Resultat version 1.1 = {"champ11":"champ11"}
Resultat version 1.2 = {"champ11":"champ11","champ12":"champ12"}

 

55.6. L'API Streaming

Depuis la version 1.6, en plus du modèle objets, Gson propose une API de streaming pour lire et écrire un document JSON. Cette API traite un document JSON sous la forme d'un ensemble d'éléments qui doivent être utilisés dans l'ordre.

Le modèle objets est accessible en utilisant la classe JsonElement. Elle est le point d'entrée qui permet de naviguer dans les éléments du modèle. Le binding objet/Json de Gson utilise cette API.

L'API streaming repose sur deux classes principales :

  • JsonReader pour lire et analyser un document JSON
  • JsonWriter pour créer un document JSON

L'API de Streaming est performante mais le code à produire pour l'utiliser est plus complexe car il est nécessaire de traiter chacun des éléments lus durant l'analyse ou à écrire.

Même si l'utilisation de l'API Streaming peut sembler lourde à mettre à oeuvre car elle nécessite beaucoup de code à écrire, elle est le moyen le plus rapide, le plus efficace et le plus puissant pour traiter des documents JSON. Elle permet aussi d'avoir le contrôle sur la manière dont le document est analysé et traité.

L'utilisation de l'API Streaming est particulièrement utile dans plusieurs cas :

  • si le document JSON est important, impliquant que son modèle objets correspondant soit intégralement monté en mémoire. Ceci est particulièrement vrai dans des environnements à ressources limitées
  • si le document doit être lu ou écrit avant qu'il ne soit intégralement disponible

Durant une analyse de type streaming, chaque élément du document JSON est représenté par un token.

Exemple : { "nom":"nom1" }

Cet exemple contient quatre tokens :

  • début d'objet : {
  • nom de propriété : "nom"
  • valeur de la propriété : "nom1"
  • fin d'objet : }

 

55.6.1. L'énumération JsonToken

Lors de l'analyse d'un document JSON par la classe JsonReader, un token est émis pour chaque élément du document.

Les différents tokens sont définis dans l'énumération com.google.gson.stream.JsonToken

Valeur

Rôle

BEGIN_ARRAY

Le début d'un tableau

BEGIN_OBJECT

Le début d'un objet

BOOLEAN

La valeur true ou false

END_ARRAY

La fermeture d'un tableau

END_DOCUMENT

La fin du document

END_OBJECT

La fermeture d'un objet

NAME

Le nom d'une propriété

NULL

La valeur null

NUMBER

Une valeur numérique qui peut correspondre à une valeur Java de type int, long ou double

STRING

Une valeur sous la forme d'une chaîne de caractères

 

55.6.2. Le parcours d'un document JSON avec la classe JsonReader

La classe com.google.gson.stream.JsonReader permet de lire et analyser un document JSON en mode stream : à chaque élément du document, elle émet un token correspondant à l'élément courant.

Un token est émis pour les différents éléments qui composent le document JSON tels que les valeurs littérales, les délimiteurs de début et de fin d'objets ou de tableau, les paires nom/valeur d'un objet, ... Les tokens sont émis au fur et à mesure de la progression dans le document JSON.

Elle ne possède qu'un seul constructeur qui attend en paramètre un objet de type Reader.

Le pilotage du parcours, qui est obligatoirement séquentiel, se fait en utilisant les différentes méthodes de la classe JsonReader :

Méthode

Rôle

void beginArray()

Consommer le prochain token en présumant qu'il correspond à un début de tableau

void beginObject()

Consommer le prochain token en présumant qu'il correspond à un début d'un objet

void close()

Fermer le Reader encapsulant la lecture du document

void endArray()

Consommer le prochain token en présumant qu'il correspond à une fin de tableau

void endObject()

Consommer le prochain token en présumant qu'il correspond à la fin d'un objet

boolean hasNext()

Renvoyer un booléen qui précise si le tableau ou l'objet courant possède encore un élément

boolean isLenient()

Renvoyer un booléen qui indique si le parseur est permissif

boolean nextBoolean()

Consommer et renvoyer la valeur du prochain token en présumant que c'est une valeur booléenne

double nextDouble()

Consommer et renvoyer la valeur du prochain token en présumant que c'est une valeur numérique de type double

int nextInt()

Consommer et renvoyer la valeur du prochain token en présumant que c'est une valeur numérique de type int

long nextLong()

Consommer et renvoyer la valeur du prochain token en présumant que c'est une valeur numérique de type long

String nextName()

Consommer et renvoyer la valeur du prochain token en présumant que c'est le nom d'une propriété

void nextNull()

Consommer et renvoyer la valeur du prochain token en présumant que c'est une valeur null

String nextString()

Consommer et renvoyer la valeur du prochain token en présumant que c'est une valeur de type chaîne de caractères

JsonToken peek()

Renvoyer le type du prochain token sans le consommer

void setLenient(boolean lenient)

Indiquer au parseur s'il doit être permissif (true) ou non (false)

void skipValue()

Consommer le prochain token en l'ignorant et en présumant que c'est une valeur


La méthode skipValue() permet d'ignorer une valeur dans le document.

Pour gérer les valeurs null, il est nécessaire d'utiliser la méthode peek() pour déterminer la nature du prochaine token : si le token est JsonToken.NULL alors la valeur est null.

La classe JsonReader n'est pas thread-safe.

Il est nécessaire d'écrire du code qui va gérer les tokens de chaque sous-structure du document Json.

 

55.6.2.1. Le traitement d'un objet

Le traitement d'un objet contient généralement les opérations suivantes :

  • invoquer la méthode beginObject()
  • itérer tant que hasNext() pour obtenir le nom du champ avec la méthode nextName() et obtenir la valeur correspondante en invoquant une des méthodes nextXXX()
  • invoquer la méthode endObject()
Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;

import com.google.gson.stream.JsonReader;

public class TestJsonReaderObjet {

  public static void main(final String[] args) {

    final String json = "{\"id\":1,\"nom\":\"nom1\",\"prenom\":\"prenom1\"}";

    try {
      final Personne personne = new Personne(0, "", "");
      final JsonReader reader = new JsonReader(new StringReader(json));

      reader.beginObject();

      while (reader.hasNext()) {
        final String name = reader.nextName();
        if (name.equals("id")) {
          personne.setId(reader.nextLong());
        } else if (name.equals("nom")) {
          personne.setNom(reader.nextString());
        } else if (name.equals("prenom")) {
          personne.setPrenom(reader.nextString());
        } else {
          reader.skipValue();
        }
      }

      reader.endObject();
      reader.close();
      System.out.println("Personne = " + personne);
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

Cet exemple est très simple car il ne gère pas certains cas particuliers comme les valeurs null.

Exemple :
// ...

    final String json = "{\"id\":1,\"nom\":\"nom1\",\"prenom\":null}";
// ...
Résultat :
Exception in thread "main" java.lang.IllegalStateException: Expected a string 
but was NULL at line 1 column 35
        at com.google.gson.stream.JsonReader.nextString(JsonReader.java:821)
        at fr.jmdoudoux.dej.gson.TestJsonReaderObjet.main(TestJsonReaderObjet.java:27)

Il est possible d'invoquer la méthode peek() et de tester le type de token retourné pour déterminer si une valeur est null.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

public class TestJsonReaderObjet {

  public static void main(final String[] args) {

    final String json = "{\"id\":1,\"nom\":\"nom1\",\"prenom\":null}";

    try {
      final Personne personne = new Personne(0, "", "");
      final JsonReader reader = new JsonReader(new StringReader(json));

      reader.beginObject();

      while (reader.hasNext()) {
        final String name = reader.nextName();
        if (name.equals("id")) {
          personne.setId(reader.nextLong());
        } else if (name.equals("nom")) {
          personne.setNom(reader.nextString());
        } else if (name.equals("prenom")) {

          if (reader.peek() == JsonToken.NULL) {
            reader.nextNull(); // ou skipValue()

            personne.setPrenom(null);
          } else {
            personne.setPrenom(reader.nextString());
          }
        } else {
          reader.skipValue();
        }
      }

      reader.endObject();
      reader.close();
      System.out.println("Personne = " + personne);
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

55.6.2.2. Le traitement d'un tableau

Le traitement d'un objet contient généralement les opérations suivantes :

  • invoquer la méthode beginArray()
  • itérer tant que hasNext() pour obtenir chaque élément en invoquant une des méthodes nextXXX().
  • invoquer la méthode endArray()
Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.stream.JsonReader;

public class TestJsonReaderTableau {

  public static void main(final String[] args) {
    final String json = "[\"element1\",\"element2\",\"element3\",\"element4\"]";

    try {
      final List<String> liste = new ArrayList<String>();
      final JsonReader reader = new JsonReader(new StringReader(json));

      reader.beginArray();

      while (reader.hasNext()) {
        liste.add(reader.nextString());
      }
      reader.endArray();
      reader.close();
      System.out.println("Liste = " + Arrays.deepToString(liste.toArray()));
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

55.6.2.3. Le traitement d'éléments composites

Lorsque le document JSON est composé des différentes structures imbriquées, il est nécessaire de les traiter séquentiellement une par une.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

public class TestJsonReader {

  public static void main(final String[] args) {
    final TestJsonReader app = new TestJsonReader();
    final String json = "[{\"id\":1,\"nom\":\"nom1\",\"prenom\":null},"
      +"{\"id\":2,\"nom\":\"nom2\",\"prenom\":\"prenom2\"}]";

    try {
      List<Personne> liste = null;
      final JsonReader reader = new JsonReader(new StringReader(json));
      liste = app.lirePersonnes(reader);
      reader.close();
      System.out.println("Liste = " + Arrays.deepToString(liste.toArray()));
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }

  public List<Personne> lirePersonnes(final JsonReader reader) throws IOException {
    final List<Personne> resultat = new ArrayList<Personne>();
    reader.beginArray();
    while (reader.hasNext()) {
      resultat.add(lirePersonne(reader));
    }
    reader.endArray();
    return resultat;
  }

  public Personne lirePersonne(final JsonReader reader) throws IOException {
    final Personne personne = new Personne(0, "", "");
    reader.beginObject();
    while (reader.hasNext()) {
      final String name = reader.nextName();
      if (name.equals("id")) {
        personne.setId(reader.nextLong());
      } else if (name.equals("nom")) {
        personne.setNom(reader.nextString());
      } else if (name.equals("prenom")) {

        if (reader.peek() == JsonToken.NULL) {
          reader.nextNull(); // ou skipValue()

          personne.setPrenom(null);
        } else {
          personne.setPrenom(reader.nextString());
        }
      } else {
        reader.skipValue();
      }
    }
    reader.endObject();
    return personne;
  }
}

Un JsonReader permet de lire les valeurs numériques indifféremment qu'elles soient sous la forme d'un nombre ou d'une chaîne de caractères dans le document JSON. Dans ce cas, il est possible d'obtenir la valeur en utilisant les méthodes nextInt(), nextLong() ou nextString().

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;

import com.google.gson.stream.JsonReader;

public class TestJsonReaderNumerique {

  public static void main(final String[] args) {
    new TestJsonReader();
    final String json = "[1,\"1\"]";

    try {
      final JsonReader reader = new JsonReader(new StringReader(json));
      reader.beginArray();
      final String valeur1 = reader.nextString();
      System.out.println("valeur1=" + valeur1);
      final long valeur2 = reader.nextLong();
      System.out.println("valeur2=" + valeur2);
      reader.endArray();
      reader.close();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
valeur1=1
valeur2=1

Ce comportement permet de contourner la façon dont JavaScript gère certaines valeurs numériques notamment les entiers longs puisque ceux-ci doivent être représentés sous la forme de chaînes de caractères.

Parfois un document JSON qui débute par un préfixe empêche son exécution direct en JavaScript. C'est notamment le cas des documents créés à partir d'une instance de type Gson produite par un GsonBuilder dont la méthode generateNonExecutableJson() a été invoquée.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.stream.JsonReader;

public class TestJsonReaderLenient {

  public static void main(final String[] args) {
    final String json = ")]}'\n[\"element1\",\"element2\"]";

    try {
      final List<String> liste = new ArrayList<String>();
      final JsonReader reader = new JsonReader(new StringReader(json));
      reader.beginArray();
      while (reader.hasNext()) {
        liste.add(reader.nextString());
      }
      reader.endArray();
      reader.close();
      System.out.println("Liste = " + Arrays.deepToString(liste.toArray()));
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) 
to accept malformed JSON at line 1 column 1 at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1505) at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1386) at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:572) at com.google.gson.stream.JsonReader.beginArray(JsonReader.java:332) at fr.jmdoudoux.dej.gson.TestJsonReaderLenient.main(TestJsonReaderLenient.java:19)

Dans ce cas, il peut être utile d'invoquer la méthode setLenient() avec en paramètre la valeur true pour permettre d'analyser le document sans erreur.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.stream.JsonReader;

public class TestJsonReaderLenient {

  public static void main(final String[] args) {
    final String json = ")]}'\n[\"element1\",\"element2\"]";

    try {
      final List<String> liste = new ArrayList<String>();
      final JsonReader reader = new JsonReader(new StringReader(json));
      reader.setLenient(true);
      reader.beginArray();
      while (reader.hasNext()) {
        liste.add(reader.nextString());
      }
      reader.endArray();
      reader.close();
      System.out.println("Liste = " + Arrays.deepToString(liste.toArray()));
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

55.6.3. La création d'un document JSON avec la classe JsonWriter

La classe JsonWriter permet de créer un document JSON en lui ajoutant chacun des éléments un par un.

Il faut créer une instance de type JsonWriter. Elle ne possède qu'un seul constructeur qui attend en paramètre une instance de type Writer qui encapsule le flux vers le document.

Elle propose plusieurs méthodes pour ajouter les différents éléments et configurer la création du document :

Méthode

Rôle

JsonWriter beginArray()

Ajouter le début d'un nouveau tableau

JsonWriter beginObject()

Ajouter le début d'un nouvel objet

void close()

Fermer le Writer avec une invocation préalable de la méthode flush()

JsonWriter endArray()

Ajouter une fin de tableau

JsonWriter endObject()

Ajouter une fin d'objet

void flush()

Demander à ce que toutes les données en cours soient envoyées au Writer

boolean getSerializeNulls()

Renvoyer un booléen qui précise si un membre d'un objet doit être ajouté au document quand sa valeur est null

boolean isHtmlSafe()

Renvoyer un booléen qui précise si l'instance est configurée pour que le document généré puisse être inclus dans un document HTML ou XML

boolean isLenient()

Renvoyer un booléen qui précise si l'objet est configuré pour être permissif sur la syntaxe

JsonWriter name(String name)

Ajouter le nom d'une propriété

JsonWriter nullValue()

Ajouter une valeur null

void setHtmlSafe(boolean htmlSafe)

Configurer l'instance pour que le document généré puisse être inclus dans un document HTML ou XML

void setIndent(String indent)

Préciser la chaîne de caractères qui sera utilisée pour l'indentation des éléments du document

void setLenient(boolean lenient)

Configurer l'instance pour être permissive sur la syntaxe du document généré

void setSerializeNulls(boolean serializeNulls)

Préciser si les valeurs null doivent être incluses dans le document

JsonWriter value(boolean value)

Ajouter une valeur de type booléen

JsonWriter value(double value)

Ajouter une valeur de type double

JsonWriter value(long value)

Ajouter une valeur de type long

JsonWriter value(Number value)

Ajouter une valeur de type numérique

JsonWriter value(String value)

Ajouter une valeur de type chaîne de caractères


La racine d'un document Json doit être soit un objet soit un tableau.

Par défaut, l'invocation d'une méthode qui va engendrer la génération d'un document JSON malformé lève une exception de type IllegalStateException.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.stream.JsonWriter;

public class TestJsonWriterErreur {

  public static void main(final String[] args) {
    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.beginArray();
      jsonWriter.value("element 1");
      jsonWriter.endArray();
      jsonWriter.endArray();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
Exception in thread "main" java.lang.IllegalStateException: Nesting problem.
        at com.google.gson.stream.JsonWriter.close(JsonWriter.java:339)
        at com.google.gson.stream.JsonWriter.endArray(JsonWriter.java:297)
        at fr.jmdoudoux.dej.gson.TestJsonWriterErreur.main(TestJsonWriterErreur.java:17)

 

55.6.3.1. L'ajout d'un objet

L'ajout d'un objet implique généralement les opérations suivantes :

  • beginObject()
  • pour chaque champ, invoquer les méthodes name() pour préciser le nom et value() pour préciser la valeur
  • endObject() ;
Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.stream.JsonWriter;

public class TestJsonWriterObjet {

  public static void main(final String[] args) {
    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.beginObject();
      jsonWriter.name("id").value(1);
      jsonWriter.name("nom").value("nom1");
      jsonWriter.name("prenom").value("prenom1");
      jsonWriter.endObject();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
json={"id":1,"nom":"nom1","prenom":"prenom1"}

Pour demander un échappement des caractères contenus dans le document JSON, ce qui permettra d'intégrer le texte dans un fichier HTML ou XML, il faut passer la valeur true à la méthode setHtmlSafe().

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.stream.JsonWriter;

public class TestJsonWriterObjet {

  public static void main(final String[] args) {
    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.setHtmlSafe(true);
      jsonWriter.beginObject();
      jsonWriter.name("id").value(1);
      jsonWriter.name("nom").value("nom1 & 2");
      jsonWriter.name("prenom").value("prenom1");
      jsonWriter.endObject();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
json={"id":1,"nom":"nom1 \u0026 2","prenom":"prenom1"}

Pour mettre une valeur null, il faut utiliser la méthode nullValue().

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.stream.JsonWriter;

public class TestJsonWriterObjet {

  public static void main(final String[] args) {
    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.beginObject();
      jsonWriter.name("id").value(1);
      jsonWriter.name("nom").value("nom1");
      jsonWriter.name("prenom").nullValue();
      jsonWriter.endObject();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
json={"id":1,"nom":"nom1 & 2","prenom":null}

 

55.6.3.2. L'ajout d'un tableau

L'ajout d'un objet implique généralement les opérations suivantes :

  • beginArray()
  • pour chaque occurrence, invoquer la méthode value() pour préciser la valeur
  • endArray() ;
Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.stream.JsonWriter;

public class TestJsonWriterTableau {

  public static void main(final String[] args) {
    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.beginArray();
      jsonWriter.value("element 1");
      jsonWriter.value("element 2");
      jsonWriter.nullValue();
      jsonWriter.endArray();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
json=["element 1","element 2",null]

 

55.6.3.3. L'ajout d'éléments composites

Lorsque le document JSON est composé des différentes structures imbriquées, il est nécessaire de les traiter séquentiellement une par une.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.stream.JsonWriter;

public class TestJsonWriter {

  public static void main(final String[] args) {
    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.beginArray();
      jsonWriter.beginObject();
      jsonWriter.name("id").value(1);
      jsonWriter.name("nom").value("nom1");
      jsonWriter.name("prenom").nullValue();
      jsonWriter.endObject();
      jsonWriter.beginObject();
      jsonWriter.name("id").value(2);
      jsonWriter.name("nom").value("nom2");
      jsonWriter.name("prenom").value("prenom2");
      jsonWriter.endObject();
      jsonWriter.endArray();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
json=[{"id":1,"nom":"nom1","prenom":null},{"id":2,"nom":"nom2","prenom":"prenom2"}]

 

55.7. Mixer l'utilisation du model objets et de l'API Streaming

Gson permet aussi de combiner les deux modèles selon les besoins. L'utilisation du modèle objets peut être mixée avec celle de l'API Streaming pour bénéficier des points forts de chacune des approches :

  • productivité du modèle objets
  • efficacité du parsing de l'API streaming

Dans ce cas, il faut utiliser l'API Streaming pour parcourir ou générer un document et utiliser les méthodes fromJson() ou toJson() de la classe Gson pour créer ou lire un élément.

Les deux API offrent des passerelles pour faciliter leurs utilisations simultanées.

 

55.7.1. Mixer l'utilisation pour analyser un document

Il est possible d'employer un JsonReader pour parcourir la structure du document et d'utiliser le modèle objets pour créer directement des objets. Ceci évite d'avoir à analyser et créer manuellement les objets tout en gardant la main sur le parcours du document.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;

public class TestGsonStreamObjetParse {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final String jsonIn = "[{\"id\":1,\"nom\":\"nom1\",\"prenom\":null},"
      +"{\"id\":2,\"nom\":\"nom2\",\"prenom\":\"prenom2\"}]";

    try {
      new StringWriter();
      final JsonReader reader = new JsonReader(new StringReader(jsonIn));

      final List<Personne> personnes = new ArrayList<Personne>();
      reader.beginArray();
      while (reader.hasNext()) {
        final Personne personne = gson.fromJson(reader, Personne.class);
        personnes.add(personne);
      }
      reader.endArray();
      reader.close();

      System.out.println("Resultat = " + Arrays.deepToString(personnes.toArray()));
      reader.close();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
Resultat = [Personne [id=1, nom=nom1, prenom=null], Personne [id=2, nom=nom2,
prenom=prenom2]]

Une autre approche possible est de lire le document avec un JsonReader et de passer ce JsonReader en paramètre de la méthode parse() d'un JsonParser qui renvoie une instance de type JsonElement. Il suffit alors de traiter ce JsonElement.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;

public class TestGsonStreamObjetParse2 {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final String jsonIn = "[{\"id\":1,\"nom\":\"nom1\",\"prenom\":null},"
      +"{\"id\":2,\"nom\":\"nom2\",\"prenom\":\"prenom2\"}]";

    try {
      new StringWriter();
      final JsonReader jsonReader = new JsonReader(new StringReader(jsonIn));
      final JsonParser jsonParser = new JsonParser();
      final JsonArray jsonArray = jsonParser.parse(jsonReader).getAsJsonArray();

      final List<Personne> personnes = new ArrayList<Personne>();
      for (final JsonElement element : jsonArray) {
        final Personne personne = gson.fromJson(element, Personne.class);
        personnes.add(personne);
      }

      System.out.println("Resultat = " + Arrays.deepToString(personnes.toArray()));
      jsonReader.close();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
Resultat = [Personne [id=1, nom=nom1, prenom=null], Personne
[id=2, nom=nom2, prenom=prenom2]]

 

55.7.2. Mixer l'utilisation pour générer un document

Il est possible d'utiliser un JsonWriter pour créer les éléments relatifs à la structure du document et de créer les différents objets en utilisant la méthode toJson() de la classe Gson.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.io.IOException;
import java.io.StringWriter;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonWriter;

public class TestGsonStreamObjetWrite {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final Personne personne1 = new Personne(1, "nom1", "prenom1");
    final Personne personne2 = new Personne(2, "nom2", "prenom2");

    try {
      final StringWriter stringWriter = new StringWriter();
      final JsonWriter jsonWriter = new JsonWriter(stringWriter);
      jsonWriter.beginArray();
      gson.toJson(personne1, Personne.class, jsonWriter);
      gson.toJson(personne2, Personne.class, jsonWriter);
      jsonWriter.endArray();
      jsonWriter.close();
      System.out.println("json=" + stringWriter.toString());
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
json=[{"id":1,"nom":"nom1","prenom":"prenom1"},{"id":2,"nom":"nom2","prenom":"prenom2"}]

 

55.8. Les concepts avancés

Généralement les opérations de sérialisation/désérialisation ne concernent pas simplement des éléments simples comme des types primitifs, des objets ou des collections mais utilisent des concepts avancés comme les generics, les collections avec des types différents ou des graphes d'objets.

 

55.8.1. La sérialisation/désérialisation de types generic

La sérialisation/désérialisation par défaut de Gson fonctionne bien si les classes ne sont pas typées avec des generics. Si la classe est définie en utilisant un generic alors le type sera perdu à cause de l'implémentation des generics qui repose sur le Type Erasure. Le type generic est perdu après la phase de compilation : il n'y a alors aucun moyen de le déterminer à l'exécution.

Exemple :
package fr.jmdoudoux.dej.gson;

public class MaClasse<T> {
  private final T      monBean;
  private final String nom;

  public MaClasse(final String nom, final T monBean) {
    super();
    this.nom = nom;
    this.monBean = monBean;
  }

  @Override
  public String toString() {
    return "MaClasse [monBean=" + this.monBean + ", nom=" + this.nom + "]";
  }

  public T getMonBean() {
    return this.monBean;
  }
}

Le problème va survenir lors de la désérialisation car Gson ne peut pas retrouver le type generic.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final MonBean monBean = new MonBean();
    monBean.setChamp10("champ 10");
    monBean.setChamp11("champ 11");
    monBean.setChamp12("champ 12");
    final MaClasse monObjet = new MaClasse<MonBean>("ma classe", monBean);
    final String json = gson.toJson(monObjet);

    System.out.println("Resultat=" + json);
    System.out.println("classe monObjet=" + monObjet.getClass().getName());

    final MaClasse monObjetLu = gson.fromJson(json, monObjet.getClass());
    System.out.println("classe monObjetLu=" + monObjetLu.getClass().getName());
    System.out.println(monObjetLu.getMonBean().toString());
    System.out.println("classe monObjetLu.getBean=" 
      + monObjetLu.getMonBean().getClass().getName());
  }
}
Résultat :
Resultat={"monBean":{"champ10":"champ 10", "champ11":"champ 11",
      "champ12":"champ 12"},"nom":"ma classe"}
classe monObjet=fr.jmdoudoux.dej.gson.MaClasse
classe monObjetLu=fr.jmdoudoux.dej.gson.MaClasse
{champ10=champ 10, champ11=champ 11, champ12=champ 12}
classe monObjetLu.getBean=com.google.gson.internal.LinkedTreeMap

Pour permettre à Gson d'instancier le bon type, ce dernier doit lui être précisé en utilisant une instance de la classe TypeToken.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

public class TestSerialisation {

  public static void main(final String[] args) {
    final Gson gson = new GsonBuilder().create();
    final MonBean monBean = new MonBean();
    monBean.setChamp10("champ 10");
    monBean.setChamp11("champ 11");
    monBean.setChamp12("champ 12");
    final MaClasse<MonBean> monObjet = new MaClasse<MonBean>("ma classe", monBean);
    final String json = gson.toJson(monObjet);

    System.out.println("Resultat=" + json);
    System.out.println("classe monObjet=" + monObjet.getClass().getName());

    final Type maClasseType = new TypeToken<MaClasse<MonBean>>() {
    }.getType();
    final MaClasse<MonBean> monObjetLu = gson.fromJson(json, maClasseType);
    System.out.println("classe monObjetLu=" + monObjetLu.getClass().getName());
    System.out.println(monObjetLu.getMonBean().toString());
    System.out.println("classe monObjetLu.getBean=" 
      + monObjetLu.getMonBean().getClass().getName());
  }
}
Résultat :
Resultat={"monBean":{"champ10":"champ 10",
      "champ11":"champ 11",
      "champ12":"champ 12"},"nom":"ma classe"}
classe monObjet=fr.jmdoudoux.dej.gson.MaClasse
classe monObjetLu=fr.jmdoudoux.dej.gson.MaClasse
MonBean [champ10=champ 10, champ11=champ 11, champ12=champ 12]
classe monObjetLu.getBean=fr.jmdoudoux.dej.gson.MonBean

La syntaxe utilisée repose sur l'instanciation d'une classe anonyme interne dont la méthode getType() est invoquée. L'instance de type java.lang.reflect.Type retournée doit être passée en paramètre de la méthode fromJson().

 

55.8.2. La sérialisation/désérialisation d'une collection contenant différents types

Même si ce n'est pas conseillé, la sérialisation d'une collection contenant différents types ne pose aucun problème particulier.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.ArrayList;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean("valeur10", "valeur11", "valeur12");

    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();
    final List maListe = new ArrayList();
    maListe.add(12345);
    maListe.add("test");
    maListe.add(monBean);

    final String json = gson.toJson(maListe);
    System.out.println("Resultat=" + json);
  }
}
Résultat :
Resultat=[12345,"test",{"champ10":"valeur10","champ11":"valeur11","champ12":"valeur12"}]

Par contre la méthode fromJson() ne va pas pouvoir désérialiser le document Json puisqu'elle n'a aucun moyen de déterminer le type de chaque élément. Normalement, il faudrait typer la collection avec un generic qui précise le type à utiliser : cependant cela oblige à n'avoir qu'un seul type d'éléments dans la collection.

Le plus simple dans ce cas est d'utiliser l'API Streaming ou d'utiliser la classe JsonParser pour parcourir chacun des éléments du tableau et de les traiter individuellement en invoquant la méthode fromJson() de la classe Gson.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;

public class TestSerialisation {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean("valeur10", "valeur11", "valeur12");

    final GsonBuilder builder = new GsonBuilder();
    final Gson gson = builder.create();
    final List maListe = new ArrayList();
    maListe.add(12345);
    maListe.add("test");
    maListe.add(monBean);

    final String json = gson.toJson(maListe);
    System.out.println("Resultat serialisation=" + json);

    final List liste = new ArrayList();
    final JsonParser parser = new JsonParser();
    final JsonArray array = parser.parse(json).getAsJsonArray();
    final int element1 = gson.fromJson(array.get(0), int.class);
    liste.add(element1);
    final String element2 = gson.fromJson(array.get(1), String.class);
    liste.add(element2);
    final MonBean element3 = gson.fromJson(array.get(2), MonBean.class);
    liste.add(element3);
    System.out.println("Resultat deserialisation=" + Arrays.deepToString(liste.toArray()));
  }
}
Résultat :
Resultat serialisation=[12345,"test",{"champ10":"valeur10", "champ11":"valeur11","
      champ12":"valeur12"}]
Resultat deserialisation=[12345, test, 
MonBean [champ10=valeur10, champ11=valeur11, champ12=valeur12]]

 

55.8.3. La désérialisation quand un même objet est référencé plusieurs fois

Lorsque l'on sérialise un graphe d'objets, il est possible qu'une même instance soit référencée plusieurs fois dans le graphe.

Dans l'exemple de cette section, un groupe est composé d'une ou plusieurs personnes.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Groupe {

  private final long           id;
  private final String         nom;
  private final List<Personne> personnes = new ArrayList<Personne>();

  public Groupe(final long id, final String nom) {
    super();
    this.nom = nom;
    this.id = id;
  }

  public String getNom() {
    return this.nom;
  }

  public long getId() {
    return this.id;
  }

  public List<Personne> getPersonnes() {
    return this.personnes;
  }

  public void ajouter(final Personne personne) {
    this.personnes.add(personne);
  }

  @Override
  public String toString() {
    return "Groupe [id=" + this.id + ", nom=" + this.nom + ", personnes="
        + Arrays.deepToString(this.personnes.toArray()) + "]";
  }
}

Une personne peut appartenir à plusieurs groupes.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final Personne[] personnes = new Personne[5];
    for (int i = 0; i < 5; i++) {
      personnes[i] = new Personne(i, "nom" + i, "prenom" + i);
    }

    final Groupe[] groupe = new Groupe[3];
    for (int i = 0; i < 3; i++) {
      groupe[i] = new Groupe(i, "groupe" + i);
    }

    final Groupes groupes = new Groupes();
    groupes.ajouterGroupe(groupe[0], personnes[0], personnes[2], personnes[4]);
    groupes.ajouterGroupe(groupe[1], personnes[1], personnes[4]);
    groupes.ajouterGroupe(groupe[2], personnes[1], personnes[2], personnes[3]);

    final GsonBuilder builder = new GsonBuilder();
    builder.setPrettyPrinting();
    builder.registerTypeAdapter(Groupes.class, new GroupesDeserializer());
    final Gson gson = builder.create();

    final String json = gson.toJson(groupes.getGroupes());
    System.out.println("Resultat serialisation=" + json);

  }
}
Résultat :
Resultat serialisation=[
  { "id": 0,
    "nom": "groupe0",
    "personnes": [
      { "id": 0, "nom": "nom0", "prenom": "prenom0" },
      { "id": 2, "nom": "nom2", "prenom": "prenom2" },
      { "id": 4, "nom": "nom4","prenom": "prenom4" } ] },
  { "id": 1,
    "nom": "groupe1",
    "personnes": [
      { "id": 1, "nom": "nom1", "prenom": "prenom1" },
      { "id": 4, "nom": "nom4", "prenom": "prenom4" } ] },
  { "id": 2,
    "nom": "groupe2",
    "personnes": [
      { "id": 1, "nom": "nom1", "prenom": "prenom1" },
      { "id": 2, "nom": "nom2", "prenom": "prenom2" },
      { "id": 3, "nom": "nom3", "prenom": "prenom3" } ] }
]

Comme une personne peut appartenir à plusieurs groupes, il y a deux solutions lors de la désérialisation :

  • créer une nouvelle instance pour chaque personne : c'est le mode de fonctionnement pas défaut de Gson
  • ne créer qu'une seule instance pour chaque personne distincte

Cela dépend du contexte mais généralement, la seconde solution est préférée. Elle nécessite de développer sa propre solution de désérialisation.

Il faut développer une classe qui va gérer les différentes instances.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Groupes {

  private final Map<Long, Personne> personnes = new HashMap<Long, Personne>();
  private final List<Groupe>        groupes   = new ArrayList<Groupe>();

  protected void ajouterPersonne(final Personne personne) {
    this.personnes.put(personne.getId(), personne);
  }

  protected void ajouterGroupe(final Groupe groupe) {
    this.groupes.add(groupe);
  }

  public void ajouterGroupe(final Groupe groupe, final Personne... personnes) {
    for (final Personne personne : personnes) {
      Personne pers = this.personnes.get(personne.getId());
      if (pers == null) {
        ajouterPersonne(personne);
        pers = personne;
      }
      groupe.ajouter(pers);
    }
    ajouterGroupe(groupe);
  }

  public void afficherStats() {
    System.out.println("Nb personnes=" + this.personnes.size());
    System.out.println("Nb groupes=" + this.groupes.size());
  }

  public List<Groupe> getGroupes() {
    return this.groupes;
  }

  @Override
  public String toString() {
    return "Groupes [" + Arrays.deepToString(this.groupes.toArray()) + "]";
  }
}

La classe Groupes encapsule les instances de type Groupe dans une collection et les instances de types Personnes dans une Map.

Lors de l'ajout d'un groupe, les personnes à associer au groupe sont d'abord recherchées dans la Map pour obtenir des instances éventuellement existantes sinon elles sont ajoutées à la Map.

Il faut ensuite définir un Deserializer personnalisé qui va extraire les données du document json, créer les différentes instances et invoquer la méthode ajouterGroupe() de la classe Groupes pour réaliser les associations.

Exemple :
package fr.jmdoudoux.dej.gson;

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;

public class GroupesDeserializer implements JsonDeserializer<Groupes> {
  @Override
  public Groupes deserialize(final JsonElement json, final Type typeOfT, 
      final JsonDeserializationContext context) throws JsonParseException {
    final Groupes resultat = new Groupes();

    if (json != JsonNull.INSTANCE) {
      final GsonBuilder builder = new GsonBuilder();
      final Gson gson = builder.create();
      final JsonArray jsonArray = json.getAsJsonArray();

      for (int i = 0; i < jsonArray.size(); i++) {
        final JsonObject jsonObject = jsonArray.get(i).getAsJsonObject();
        final long id = jsonObject.get("id").getAsLong();
        final String nom = jsonObject.get("nom").getAsString();
        final Groupe groupe = new Groupe(id, nom);

        final JsonArray personnesArray = jsonObject.getAsJsonArray("personnes");
        final Personne[] personnes = new Personne[personnesArray.size()];
        for (int j = 0; j < personnesArray.size(); j++) {
          final JsonObject jsonPersonne = personnesArray.get(j).getAsJsonObject();
          final Personne personne = gson.fromJson(jsonPersonne, Personne.class);
          personnes[j] = personne;
        }
        resultat.ajouterGroupe(groupe, personnes);
      }
    }
    return resultat;
  }
}

Il suffit alors d'associer le Deserializer à l'instance de type Gson et de l'utiliser pour désérialiser le document json.

Exemple :
package fr.jmdoudoux.dej.gson;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TestSerialisation {

  public static void main(final String[] args) {
    final Personne[] personnes = new Personne[5];
    for (int i = 0; i < 5; i++) {
      personnes[i] = new Personne(i, "nom" + i, "prenom" + i);
    }

    final Groupe[] groupe = new Groupe[3];
    for (int i = 0; i < 3; i++) {
      groupe[i] = new Groupe(i, "groupe" + i);
    }

    final Groupes groupes = new Groupes();
    groupes.ajouterGroupe(groupe[0], personnes[0], personnes[2], personnes[4]);
    groupes.ajouterGroupe(groupe[1], personnes[1], personnes[4]);
    groupes.ajouterGroupe(groupe[2], personnes[1], personnes[2], personnes[3]);

    final GsonBuilder builder = new GsonBuilder();
    builder.setPrettyPrinting();
    builder.registerTypeAdapter(Groupes.class, new GroupesDeserializer());
    final Gson gson = builder.create();

    final String json = gson.toJson(groupes.getGroupes());

    final Groupes groupesDes = gson.fromJson(json, Groupes.class);
    System.out.println("Deserialisation avec serializer = " + groupesDes);
    groupesDes.afficherStats();
  }
}
Résultat :
Deserialisation avec serializer = Groupes [[Groupe [id=0, nom=groupe0, 
      personnes=[Personne [id=0, nom=nom0, prenom=prenom0], 
      Personne [id=2, nom=nom2, prenom=prenom2], 
      Personne [id=4, nom=nom4, prenom=prenom4]]], 
      Groupe [id=1, nom=groupe1, personnes=[Personne [id=1, nom=nom1, prenom=prenom1], 
      Personne [id=4, nom=nom4, prenom=prenom4]]], 
      Groupe [id=2, nom=groupe2, personnes=[Personne [id=1, nom=nom1, prenom=prenom1], 
      Personne [id=2, nom=nom2, prenom=prenom2], 
      Personne [id=3, nom=nom3, prenom=prenom3]]]]]
Nb personnes=5
Nb groupes=3

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

78 commentaires Donner une note à l´article (5)

 

Copyright (C) 1999-2022 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.