web-dev-qa-db-fra.com

Gson sérialise une liste d'objets polymorphes

J'essaie de sérialiser/désérialiser un objet, qui implique un polymorphisme, en JSON en utilisant Gson.

Voici mon code de sérialisation:

ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");

ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");

lobbyObj.addChild(batchOp);

Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));

Voici le résultat:

 {"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch"}]}

La sérialisation fonctionne principalement, sauf qu'il manque le contenu des membres hérités (en particulier obix:BatchIn et obixBatchout chaînes manquantes). Voici ma classe de base:

public class ObixBaseObj  {
    protected String obix;
    private String display;
    private String displayName;
    private ArrayList<ObixBaseObj> children;

    public ObixBaseObj()
    {
        obix = "obj";
    }

    public void setName(String name) {
        this.name = name;
    }
        ...
}

Voici à quoi ressemble ma classe héritée (ObixOp):

public class ObixOp extends ObixBaseObj {
    private String in;
    private String out;

    public ObixOp() {
        obix = "op";
    }
    public ObixOp(String in, String out) {
        obix = "op";
        this.in = in;
        this.out = out;
    }
    public String getIn() {
        return in;
    }
    public void setIn(String in) {
        this.in = in;
    }
    public String getOut() {
        return out;
    }
    public void setOut(String out) {
        this.out = out;
    }
}

Je me rends compte que je pourrais utiliser un adaptateur pour cela, mais le problème est que je sérialise une collection de type de classe de base ObixBaseObj. Il y a environ 25 classes qui en héritent. Comment puis-je faire fonctionner cela avec élégance?

42
l46kok

Je pense qu'un sérialiseur/désérialiseur personnalisé est la seule façon de procéder et j'ai essayé de vous proposer le moyen le plus compact pour le réaliser que j'ai trouvé. Je m'excuse de ne pas utiliser vos classes, mais l'idée est la même (je voulais juste au moins 1 classe de base et 2 classes étendues).

BaseClass.Java

public class BaseClass{

    @Override
    public String toString() {
        return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]";
    }

    public ArrayList<BaseClass> list = new ArrayList<BaseClass>();

    protected String isA="BaseClass"; 
    public int x;

 }

ExtendedClass1.Java

public class ExtendedClass1 extends BaseClass{

    @Override
    public String toString() {
       return "ExtendedClass1 [total=" + total + ", number=" + number
            + ", list=" + list + ", isA=" + isA + ", x=" + x + "]";
    }

    public ExtendedClass1(){
        isA = "ExtendedClass1";
    }

    public Long total;
    public Long number;

}

ExtendedClass2.Java

public class ExtendedClass2 extends BaseClass{

    @Override
    public String toString() {
      return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA="
            + isA + ", x=" + x + "]";
    }

    public ExtendedClass2(){
        isA = "ExtendedClass2";
    }

    public Long total;

}

CustomDeserializer.Java

public class CustomDeserializer implements JsonDeserializer<List<BaseClass>> {

    private static Map<String, Class> map = new TreeMap<String, Class>();

    static {
        map.put("BaseClass", BaseClass.class);
        map.put("ExtendedClass1", ExtendedClass1.class);
        map.put("ExtendedClass2", ExtendedClass2.class);
    }

    public List<BaseClass> deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException {

        List list = new ArrayList<BaseClass>();
        JsonArray ja = json.getAsJsonArray();

        for (JsonElement je : ja) {

            String type = je.getAsJsonObject().get("isA").getAsString();
            Class c = map.get(type);
            if (c == null)
                throw new RuntimeException("Unknow class: " + type);
            list.add(context.deserialize(je, c));
        }

        return list;

    }

}

CustomSerializer.Java

public class CustomSerializer implements JsonSerializer<ArrayList<BaseClass>> {

    private static Map<String, Class> map = new TreeMap<String, Class>();

    static {
        map.put("BaseClass", BaseClass.class);
        map.put("ExtendedClass1", ExtendedClass1.class);
        map.put("ExtendedClass2", ExtendedClass2.class);
    }

    @Override
    public JsonElement serialize(ArrayList<BaseClass> src, Type typeOfSrc,
            JsonSerializationContext context) {
        if (src == null)
            return null;
        else {
            JsonArray ja = new JsonArray();
            for (BaseClass bc : src) {
                Class c = map.get(bc.isA);
                if (c == null)
                    throw new RuntimeException("Unknow class: " + bc.isA);
                ja.add(context.serialize(bc, c));

            }
            return ja;
        }
    }
}

et maintenant c'est le code que j'ai exécuté pour tester le tout:

public static void main(String[] args) {

  BaseClass c1 = new BaseClass();
  ExtendedClass1 e1 = new ExtendedClass1();
  e1.total = 100L;
  e1.number = 5L;
  ExtendedClass2 e2 = new ExtendedClass2();
  e2.total = 200L;
  e2.x = 5;
  BaseClass c2 = new BaseClass();

  c1.list.add(e1);
  c1.list.add(e2);
  c1.list.add(c2);


  List<BaseClass> al = new ArrayList<BaseClass>();

  // this is the instance of BaseClass before serialization
  System.out.println(c1);

  GsonBuilder gb = new GsonBuilder();

  gb.registerTypeAdapter(al.getClass(), new CustomDeserializer());
  gb.registerTypeAdapter(al.getClass(), new CustomSerializer());
  Gson gson = gb.create();

  String json = gson.toJson(c1);
  // this is the corresponding json
  System.out.println(json);

  BaseClass newC1 = gson.fromJson(json, BaseClass.class);

  System.out.println(newC1);

}

Voici mon exécution:

BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
{"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0}
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]

Quelques explications: l'astuce est faite par un autre Gson à l'intérieur du sérialiseur/désérialiseur. J'utilise juste le champ isA pour repérer la bonne classe. Pour aller plus vite, j'utilise une carte pour associer la chaîne isA à la classe correspondante. Ensuite, je fais la sérialisation/désérialisation appropriée en utilisant le deuxième objet Gson. Je l'ai déclaré comme statique afin que vous ne ralentissiez pas la sérialisation/désérialisation avec l'allocation multiple de Gson.

Pro En fait, vous n'écrivez pas de code plus que cela, vous laissez Gson faire tout le travail. Vous devez juste vous rappeler de mettre une nouvelle sous-classe dans les cartes (l'exception vous le rappelle).

Contre Vous vous avez deux cartes. Je pense que mon implémentation peut être un peu affinée pour éviter les duplications de cartes, mais je vous les laisse (ou au futur éditeur, le cas échéant).

Peut-être que vous souhaitez unifier la sérialisation et la désérialisation en un objet unique, vous devriez vérifier la classe TypeAdapter ou expérimenter avec un objet qui implémente les deux interfaces.

41
giampaolo

Il existe une solution simple: RuntimeTypeAdapterFactory de Gson (from com.google.code.gson:gson-extras:$gsonVersion). Vous n'avez pas besoin d'écrire de sérialiseur, cette classe fonctionne tout pour vous. Essayez ceci avec votre code:

    ObixBaseObj lobbyObj = new ObixBaseObj();
    lobbyObj.setIs("obix:Lobby");

    ObixOp batchOp = new ObixOp();
    batchOp.setName("batch");
    batchOp.setIn("obix:BatchIn");
    batchOp.setOut("obix:BatchOut");

    lobbyObj.addChild(batchOp);

    RuntimeTypeAdapterFactory<ObixBaseObj> adapter = 
                    RuntimeTypeAdapterFactory
                   .of(ObixBaseObj.class)
                   .registerSubtype(ObixBaseObj.class)
                   .registerSubtype(ObixOp.class);


    Gson gson2=new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
    Gson gson = new Gson();
    System.out.println(gson.toJson(lobbyObj));
    System.out.println("---------------------");
    System.out.println(gson2.toJson(lobbyObj));

}

Sortie:

{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch","children":[]}]}
---------------------
{
  "type": "ObixBaseObj",
  "obix": "obj",
  "is": "obix:Lobby",
  "children": [
    {
      "type": "ObixOp",
      "in": "obix:BatchIn",
      "out": "obix:BatchOut",
      "obix": "op",
      "name": "batch",
      "children": []
    }
  ]
}

EDIT: Meilleur exemple de travail.

Vous avez dit qu'il y a environ 25 classes qui héritent de ObixBaseObj.

Nous commençons à écrire une nouvelle classe, GsonUtils

public class GsonUtils {

    private static final GsonBuilder gsonBuilder = new GsonBuilder()
            .setPrettyPrinting();

    public static void registerType(
            RuntimeTypeAdapterFactory<?> adapter) {
        gsonBuilder.registerTypeAdapterFactory(adapter);
    }

    public static Gson getGson() {
        return gsonBuilder.create();
    }

Chaque fois que nous avons besoin d'un objet Gson, au lieu d'appeler new Gson(), nous appellerons

GsonUtils.getGson()

Nous ajoutons ce code à ObixBaseObj:

public class ObixBaseObj {
    protected String obix;
    private String display;
    private String displayName;
    private String name;
    private String is;
    private ArrayList<ObixBaseObj> children = new ArrayList<ObixBaseObj>();
    // new code
    private static final RuntimeTypeAdapterFactory<ObixBaseObj> adapter = 
            RuntimeTypeAdapterFactory.of(ObixBaseObj.class);

    private static final HashSet<Class<?>> registeredClasses= new HashSet<Class<?>>();

    static {
        GsonUtils.registerType(adapter);
    }

    private synchronized void registerClass() {
        if (!registeredClasses.contains(this.getClass())) {
            registeredClasses.add(this.getClass());
            adapter.registerSubtype(this.getClass());
        }
    }
    public ObixBaseObj() {
        registerClass();
        obix = "obj";
    }

Pourquoi? car chaque fois que cette classe ou une classe enfant de ObixBaseObj est instanciée, la classe qu'elle va être enregistrée dans le RuntimeTypeAdapter

Dans les classes enfants, seul un changement minimal est nécessaire:

public class ObixOp extends ObixBaseObj {
    private String in;
    private String out;

    public ObixOp() {
        super();
        obix = "op";
    }

    public ObixOp(String in, String out) {
        super();
        obix = "op";
        this.in = in;
        this.out = out;
    }

Exemple de travail:

public static void main(String[] args) {

        ObixBaseObj lobbyObj = new ObixBaseObj();
        lobbyObj.setIs("obix:Lobby");

        ObixOp batchOp = new ObixOp();
        batchOp.setName("batch");
        batchOp.setIn("obix:BatchIn");
        batchOp.setOut("obix:BatchOut");

        lobbyObj.addChild(batchOp);



        Gson gson = GsonUtils.getGson();
        System.out.println(gson.toJson(lobbyObj));

    }

Sortie:

{
  "type": "ObixBaseObj",
  "obix": "obj",
  "is": "obix:Lobby",
  "children": [
    {
      "type": "ObixOp",
      "in": "obix:BatchIn",
      "out": "obix:BatchOut",
      "obix": "op",
      "name": "batch",
      "children": []
    }
  ]
}

J'espère que ça aide.

49
rpax

J'apprécie les autres réponses ici qui m'ont conduit sur la voie de la résolution de ce problème. J'ai utilisé une combinaison de RuntimeTypeAdapterFactory avec Reflection .

J'ai également créé une classe d'assistance pour m'assurer qu'un Gson correctement configuré a été utilisé.

Dans un bloc statique à l'intérieur de la classe GsonHelper, j'ai le code suivant parcourir mon projet pour trouver et enregistrer tous les types appropriés. Tous mes objets qui passeront par JSON-ification sont un sous-type de Jsonable. Vous souhaiterez modifier les éléments suivants:

  1. my.project dans Reflections doit être le nom de votre package.
  2. Jsonable.class est ma classe de base. Remplacez le vôtre.
  3. J'aime que le champ affiche le nom canonique complet, mais clairement si vous ne le souhaitez pas/n'en avez pas besoin, vous pouvez omettre cette partie de l'appel pour enregistrer le sous-type. La même chose vaut pour className dans le RuntimeAdapterFactory; J'ai déjà des éléments de données utilisant le champ type.

    private static final GsonBuilder gsonBuilder = new GsonBuilder()
        .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
        .excludeFieldsWithoutExposeAnnotation()
        .setPrettyPrinting();
    
    static {
    Reflections reflections = new Reflections("my.project");
    
    Set<Class<? extends Jsonable>> allTypes = reflections.getSubTypesOf(Jsonable.class);
    for (Class< ? extends Jsonable> serClass : allTypes){
        Set<?> subTypes = reflections.getSubTypesOf(serClass);
        if (subTypes.size() > 0){
            RuntimeTypeAdapterFactory<?> adapterFactory = RuntimeTypeAdapterFactory.of(serClass, "className");
            for (Object o : subTypes ){
                Class c = (Class)o;
                adapterFactory.registerSubtype(c, c.getCanonicalName());
            }
            gsonBuilder.registerTypeAdapterFactory(adapterFactory);
        }
    }
    }
    
    public static Gson getGson() {
        return gsonBuilder.create();
    }
    
5
Jeff