107 votes

Polymorphisme avec gson

J'ai un problème pour désérialiser une chaîne json avec Gson. Je reçois un tableau de commandes. La commande peut être start, stop ou un autre type de commande. Naturellement, j'ai le polymorphisme, et les commandes start/stop héritent de command.

Comment puis-je le sérialiser vers l'objet de commande correct en utilisant gson ?

Il semble que je n'obtienne que le type de base, c'est-à-dire le type déclaré, et jamais le type d'exécution.

0 votes

122voto

C'est un peu tard mais j'ai dû faire exactement la même chose aujourd'hui. Donc, d'après mes recherches et lorsque vous utilisez gson-2.0, vous ne voulez vraiment pas utiliser l'option registerTypeHierarchyAdapter mais plutôt la méthode plus banale registerTypeAdapter . Et vous n'avez certainement pas besoin de faire instanceofs ou écrire des adaptateurs pour les classes dérivées : un seul adaptateur pour la classe ou l'interface de base, à condition bien sûr que vous soyez satisfait de la sérialisation par défaut des classes dérivées. Quoi qu'il en soit, voici le code (paquetage et importations enlevés) (également disponible dans github ) :

La classe de base (interface dans mon cas) :

public interface IAnimal { public String sound(); }

Les deux classes dérivées, Cat :

public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}

Et le chien :

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}

L'adaptateur IAnimalAdapter :

public class IAnimalAdapter implements JsonSerializer<IAnimal>, JsonDeserializer<IAnimal>{

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(IAnimal src, Type typeOfSrc,
            JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public IAnimal deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

Et la classe Test :

public class Test {

    public static void main(String[] args) {
        IAnimal animals[] = new IAnimal[]{new Cat("Kitty"), new Dog("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(IAnimal.class, new IAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, IAnimal.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            IAnimal animal2 = gsonExt.fromJson(animalJson, IAnimal.class);
            System.out.println(animal2.sound());
        }
    }
}

Lorsque vous exécutez le Test::main, vous obtenez le résultat suivant :

serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Cat","INSTANCE":{"name":"Kitty"}}
Kitty : "meaow"
serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Dog","INSTANCE":{"name":"Brutus","ferocity":5}}
Brutus : "bark" (ferocity level:5)

J'ai fait ce qui précède en utilisant le registerTypeHierarchyAdapter mais cela semblait nécessiter l'implémentation de classes de sérialiseur/désérialiseur personnalisées DogAdapter et CatAdapter, qui sont difficiles à maintenir chaque fois que l'on veut ajouter un nouveau champ à Dog ou à Cat.

1 votes

Juste un petit ajout : Pour que cela fonctionne, assurez-vous que vos classes ne sont PAS définies en interne (sous-classe).

5 votes

Notez que la sérialisation des noms de classe et la désérialisation (à partir d'une entrée utilisateur) à l'aide de Class.forName peut présenter des implications en matière de sécurité dans certaines situations, et est donc déconseillée par l'équipe de développement de Gson. code.google.com/p/google-gson/issues/detail?id=340#c2

4 votes

Comment avez-vous réussi à ne pas avoir une boucle infinie dans la sérialisation, vous appelez context.serialize(src) ; ce qui va invoquer votre adaptateur à nouveau. C'est ce qui s'est passé dans mon code similaire.

13voto

Programmer Bruce Points 16306

Gson dispose actuellement d'un mécanisme permettant de enregistrer un adaptateur de hiérarchie de types qu'il peut être configuré pour une simple désérialisation polymorphe, mais je ne vois pas comment c'est le cas, car un adaptateur de hiérarchie de type semble être un sérialiseur/désérialiseur/créateur d'instance combiné, laissant les détails de la création d'instance au codeur, sans fournir aucun enregistrement de type polymorphe réel.

Il semble que Gson aura bientôt le RuntimeTypeAdapter pour une désérialisation polymorphe plus simple. Voir http://code.google.com/p/google-gson/issues/detail?id=231 pour plus d'informations.

Si l'utilisation de la nouvelle RuntimeTypeAdapter n'est pas possible, et que vous devez utiliser Gson, alors je pense que vous devrez mettre en place votre propre solution, en enregistrant un désérialiseur personnalisé soit comme adaptateur de hiérarchie de type, soit comme adaptateur de type. Voici un exemple de ce type.

// output:
//     Starting machine1
//     Stopping machine2

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

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

public class Foo
{
  // [{"machine_name":"machine1","command":"start"},{"machine_name":"machine2","command":"stop"}]
  static String jsonInput = "[{\"machine_name\":\"machine1\",\"command\":\"start\"},{\"machine_name\":\"machine2\",\"command\":\"stop\"}]";

  public static void main(String[] args)
  {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    CommandDeserializer deserializer = new CommandDeserializer("command");
    deserializer.registerCommand("start", Start.class);
    deserializer.registerCommand("stop", Stop.class);
    gsonBuilder.registerTypeAdapter(Command.class, deserializer);
    Gson gson = gsonBuilder.create();
    Command[] commands = gson.fromJson(jsonInput, Command[].class);
    for (Command command : commands)
    {
      command.execute();
    }
  }
}

class CommandDeserializer implements JsonDeserializer<Command>
{
  String commandElementName;
  Gson gson;
  Map<String, Class<? extends Command>> commandRegistry;

  CommandDeserializer(String commandElementName)
  {
    this.commandElementName = commandElementName;
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    gson = gsonBuilder.create();
    commandRegistry = new HashMap<String, Class<? extends Command>>();
  }

  void registerCommand(String command, Class<? extends Command> commandInstanceClass)
  {
    commandRegistry.put(command, commandInstanceClass);
  }

  @Override
  public Command deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {
    try
    {
      JsonObject commandObject = json.getAsJsonObject();
      JsonElement commandTypeElement = commandObject.get(commandElementName);
      Class<? extends Command> commandInstanceClass = commandRegistry.get(commandTypeElement.getAsString());
      Command command = gson.fromJson(json, commandInstanceClass);
      return command;
    }
    catch (Exception e)
    {
      throw new RuntimeException(e);
    }
  }
}

abstract class Command
{
  String machineName;

  Command(String machineName)
  {
    this.machineName = machineName;
  }

  abstract void execute();
}

class Stop extends Command
{
  Stop(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Stopping " + machineName);
  }
}

class Start extends Command
{
  Start(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Starting " + machineName);
  }
}

0 votes

Si vous pouvez modifier les API, notez que Jackson dispose actuellement d'un mécanisme de désérialisation polymorphe relativement simple. J'ai posté quelques exemples à programmerbruce.blogspot.com/2011/05/

0 votes

RuntimeTypeAdapter est maintenant terminé, malheureusement il ne semble pas qu'il soit encore dans le noyau de Gson :-(

8voto

user2242263 Points 31

Marcus Junius Brutus a donné une excellente réponse (merci !). Pour étendre son exemple, vous pouvez rendre sa classe d'adaptateur générique pour qu'elle fonctionne pour tous les types d'objets (pas seulement IAnimal) avec les changements suivants :

class InheritanceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T>
{
....
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
....
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
....
}

Et dans la classe test :

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}

1 votes

Après avoir mis en œuvre sa solution, j'ai eu l'idée de faire exactement ceci :-)

7voto

k.c. sham Points 96

Le GSON propose ici un bon scénario de test montrant comment définir et enregistrer un adaptateur de hiérarchie de types.

http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739

Pour l'utiliser, faites ceci :

    gson = new GsonBuilder()
          .registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor())
          .create();

La méthode Serialize de l'adaptateur peut être une vérification en cascade if-else du type qu'il sérialise.

    JsonElement result = new JsonObject();

    if (src instanceof SliderQuestion) {
        result = context.serialize(src, SliderQuestion.class);
    }
    else if (src instanceof TextQuestion) {
        result = context.serialize(src, TextQuestion.class);
    }
    else if (src instanceof ChoiceQuestion) {
        result = context.serialize(src, ChoiceQuestion.class);
    }

    return result;

La désérialisation est un peu compliquée. Dans l'exemple du test unitaire, il vérifie l'existence d'attributs révélateurs pour décider de la classe à désérialiser. Si vous pouvez changer la source de l'objet que vous sérialisez, vous pouvez ajouter un attribut 'classType' à chaque instance qui contient le FQN du nom de la classe d'instance. Mais tout cela n'est pas du tout orienté objet.

6voto

db80 Points 109

Google a publié son propre RuntimeTypeAdapterFactory pour gérer le polymorphisme mais malheureusement elle ne fait pas partie du noyau de gson (vous devez copier et coller la classe dans votre projet).

Ejemplo:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Aquí J'ai mis en ligne un exemple fonctionnel complet de ce système en utilisant les modèles Animal, Chien et Chat.

Je pense qu'il est préférable de s'appuyer sur cet adaptateur plutôt que de le réimplémenter à partir de zéro.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X