Polymorphisme avec gson

103

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

Comment puis-je le sérialiser à nouveau dans l'objet de commande correct à l'aide de gson?

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

Sophie
la source

Réponses:

120

C'est un peu tard mais j'ai dû faire exactement la même chose aujourd'hui. Donc, sur la base de mes recherches et lors de l'utilisation de gson-2.0, vous ne voulez vraiment pas utiliser la méthode registerTypeHierarchyAdapter , mais plutôt le registerTypeAdapter plus banal . Et vous n'avez certainement pas besoin de faire des instances ou d'écrire des adaptateurs pour les classes dérivées: juste un adaptateur pour la classe de base ou l'interface, à 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 (package et importations supprimé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 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 + ")";
    }
}

Le 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 Test :: main, vous obtenez la sortie suivante:

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 également fait ce qui précède en utilisant la méthode registerTypeHierarchyAdapter , mais cela semblait nécessiter l'implémentation de classes de sérialiseur / désérialiseur DogAdapter et CatAdapter personnalisées, ce qui est difficile à gérer chaque fois que vous souhaitez ajouter un autre champ à Dog ou à Cat.

Marcus Junius Brutus
la source
5
Notez que la sérialisation des noms de classe et la désérialisation (à partir de l'entrée utilisateur) à l'aide de Class.forName peuvent présenter des implications de sécurité dans certaines situations, et sont donc déconseillées par l'équipe de développement de Gson. code.google.com/p/google-gson/issues/detail?id=340#c2
Programmeur Bruce
4
Comment avez-vous réussi à ne pas obtenir une boucle infinie dans la sérialisation, vous appelez context.serialize (src); qui invoquera à nouveau votre adaptateur. C'est ce qui s'est passé dans mon code similaire.
che javara
6
Faux. Cette solution ne fonctionne pas. Si vous appelez context.serialize de quelque manière que ce soit, vous vous retrouverez avec une récursivité infinie. Je me demande pourquoi les gens publient sans tester le code. J'ai essayé avec 2.2.1. Voir le bug décrit dans stackoverflow.com/questions/13244769/...
che javara
4
@MarcusJuniusBrutus J'ai exécuté votre code et il semble que cela ne fonctionne que dans ce cas particulier - parce que vous avez défini une super-interface IAnimal et que IAnimalAdapter l'utilise. Si à la place vous n'aviez que «Cat», vous obtiendrez le problème de récursivité infinie. Cette solution ne fonctionne donc toujours pas dans le cas général - uniquement lorsque vous êtes en mesure de définir une interface commune. Dans mon cas, il n'y avait pas d'interface, j'ai donc dû utiliser une approche différente avec TypeAdapterFactory.
che javara
2
Utilisateur src.getClass (). GetName () au lieu de src.getClass (). GetCanonicalName (). Cela signifie que le code fonctionnera également pour les classes internes / imbriquées.
mR_fr0g
13

Gson dispose actuellement d'un mécanisme pour enregistrer un adaptateur de hiérarchie de types qui 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 types semble être simplement un créateur de sérialiseur / désérialiseur / instance combiné, laissant les détails de la création de l'instance au codeur, sans fournir d'enregistrement de type polymorphe réel.

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

Si l'utilisation du nouveau RuntimeTypeAdaptern'est pas possible et que vous devez utiliser Gson, je pense que vous devrez lancer votre propre solution, en enregistrant un désérialiseur personnalisé en tant qu'adaptateur de hiérarchie de types ou en tant qu'adaptateur de type. Voici un exemple.

// 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);
  }
}
Programmeur Bruce
la source
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 sur programmerbruce.blogspot.com/2011/05/…
Programmer Bruce
RuntimeTypeAdapterest maintenant terminé, malheureusement, il ne semble pas encore être dans le noyau de Gson. :-(
Jonathan
8

Marcus Junius Brutus a eu 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 modifications suivantes:

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 de test:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}
user2242263
la source
1
Après avoir mis en œuvre sa solution, ma prochaine pensée était de faire exactement ceci :-)
David Levy
7

GSON a un très bon cas de test ici 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, procédez comme suit:

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

La méthode de sérialisation de l'adaptateur peut être une vérification if-else en cascade du type à sérialiser.

    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 piratée. Dans l'exemple de test unitaire, il vérifie l'existence d'attributs révélateurs pour décider dans quelle classe désérialiser. Si vous pouvez modifier 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. Ceci est cependant très peu orienté objet.

kc sham
la source
4

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

Exemple:

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

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

Ici, j'ai posté un exemple de travail complet en utilisant les modèles Animal, Dog et Cat.

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.

db80
la source
2

Le temps a passé, mais je n'ai pas pu trouver une très bonne solution en ligne. Voici une petite torsion sur la solution de @ MarcusJuniusBrutus, qui évite la récursivité infinie.

Gardez le même désérialiseur, mais supprimez le sérialiseur -

public class IAnimalAdapter implements JsonDeSerializer<IAnimal> {
  private static final String CLASSNAME = "CLASSNAME";
  private static final String INSTANCE  = "INSTANCE";

  @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);
  }
}

Ensuite, dans votre classe d'origine, ajoutez un champ avec @SerializedName("CLASSNAME"). L'astuce consiste maintenant à initialiser cela dans le constructeur de la classe de base , alors transformez votre interface en une classe abstraite.

public abstract class IAnimal {
  @SerializedName("CLASSNAME")
  public String className;

  public IAnimal(...) {
    ...
    className = this.getClass().getName();
  }
}

La raison pour laquelle il n'y a pas de récursivité infinie ici est que nous passons la classe d'exécution réelle (c'est-à-dire Dog not IAnimal) à context.deserialize. Cela n'appellera pas notre adaptateur de type, tant que nous utilisons registerTypeAdapteret nonregisterTypeHierarchyAdapter

Ginandi
la source
2

Réponse mise à jour - Meilleures parties de toutes les autres réponses

Je décris des solutions pour divers cas d'utilisation et je traiterai la récursion infinie problème aussi bien

  • Cas 1: Vous contrôlez les classes , c'est-à-dire que vous écrivez les vôtres Cat, Dogainsi que lesIAnimal interface. Vous pouvez simplement suivre la solution fournie par @ marcus-junius-brutus (la réponse la mieux notée)

    Il n'y aura pas de récursivité infinie s'il existe une interface de base commune comme IAnimal

    Mais que faire si je ne veux pas implémenter l' IAnimalinterface ou une telle interface?

    Ensuite, @ marcus-junius-brutus (la réponse la mieux notée) produira une erreur de récurrence infinie. Dans ce cas, nous pouvons faire quelque chose comme ci-dessous.

    Nous devrons créer un constructeur de copie à l'intérieur de la classe de base et une sous-classe wrapper comme suit:

.

// Base class(modified)
public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }
    // COPY CONSTRUCTOR
    public Cat(Cat cat) {
        this.name = cat.name;
    }

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



    // The wrapper subclass for serialization
public class CatWrapper extends Cat{


    public CatWrapper(String name) {
        super(name);
    }

    public CatWrapper(Cat cat) {
        super(cat);
    }
}

Et le sérialiseur pour le type Cat:

public class CatSerializer implements JsonSerializer<Cat> {

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

        // Essentially the same as the type Cat
        JsonElement catWrapped = context.serialize(new CatWrapper(src));

        // Here, we can customize the generated JSON from the wrapper as we want.
        // We can add a field, remove a field, etc.


        return modifyJSON(catWrapped);
    }

    private JsonElement modifyJSON(JsonElement base){
        // TODO: Modify something
        return base;
    }
}

Alors, pourquoi un constructeur de copie?

Eh bien, une fois que vous avez défini le constructeur de copie, peu importe combien la classe de base change, votre wrapper continuera avec le même rôle. Deuxièmement, si nous ne définissons pas un constructeur de copie et que nous sous-classons simplement la classe de base, nous devrons alors "parler" en termes de classe étendue, c'est-à-dire,CatWrapper . Il est fort possible que vos composants parlent en termes de classe de base et non de type wrapper.

Existe-t-il une alternative simple?

Bien sûr, il a maintenant été introduit par Google - voici la RuntimeTypeAdapterFactorymise en œuvre:

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

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

Ici, vous devrez introduire un champ appelé "type" dans Animalet la valeur du même à l'intérieur Dogpour être "chien", Catpour être "chat"

Exemple complet: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html

  • Cas 2: Vous ne contrôlez pas les classes . Vous rejoignez une entreprise ou utilisez une bibliothèque où les classes sont déjà définies et votre manager ne veut pas que vous les changiez de quelque manière que ce soit - Vous pouvez sous-classer vos classes et leur faire implémenter une interface de marqueurs commune (qui n'a pas de méthode ) comme AnimalInterface.

    Ex:

.

// The class we are NOT allowed to modify

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 + ")";
    }
}


// The marker interface

public interface AnimalInterface {
}

// The subclass for serialization

public class DogWrapper  extends Dog implements AnimalInterface{

    public DogWrapper(String name, int ferocity) {
        super(name, ferocity);
    }

}

// The subclass for serialization

public class CatWrapper extends Cat implements AnimalInterface{


    public CatWrapper(String name) {
        super(name);
    }
}

Donc, nous utiliserions CatWrapperau lieu de Cat, DogWrapperau lieu Doget AlternativeAnimalAdapterau lieu deIAnimalAdapter

// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`

public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {

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

    @Override
    public JsonElement serialize(AnimalInterface 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 AnimalInterface 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);
    }
}

Nous effectuons un test:

public class Test {

    public static void main(String[] args) {

        // Note that we are using the extended classes instead of the base ones
        IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
        }
    }
}

Production:

serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}
Manish Kumar Sharma
la source
1

Si vous souhaitez gérer un TypeAdapter pour un type et un autre pour son sous-type, vous pouvez utiliser un TypeAdapterFactory comme ceci:

public class InheritanceTypeAdapterFactory implements TypeAdapterFactory {

    private Map<Class<?>, TypeAdapter<?>> adapters = new LinkedHashMap<>();

    {
        adapters.put(Animal.class, new AnimalTypeAdapter());
        adapters.put(Dog.class, new DogTypeAdapter());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        TypeAdapter<T> typeAdapter = null;
        Class<?> currentType = Object.class;
        for (Class<?> type : adapters.keySet()) {
            if (type.isAssignableFrom(typeToken.getRawType())) {
                if (currentType.isAssignableFrom(type)) {
                    currentType = type;
                    typeAdapter = (TypeAdapter<T>)adapters.get(type);
                }
            }
        }
        return typeAdapter;
    }
}

Cette usine enverra le TypeAdapter le plus précis

r3n0j
la source
0

Si vous combinez la réponse de Marcus Junius Brutus avec la modification de user2242263, vous pouvez éviter d'avoir à spécifier une grande hiérarchie de classes dans votre adaptateur en définissant votre adaptateur comme travaillant sur un type d'interface. Vous pouvez ensuite fournir des implémentations par défaut de toJSON () et fromJSON () dans votre interface (qui n'inclut que ces deux méthodes) et faire en sorte que chaque classe dont vous avez besoin pour sérialiser implémente votre interface. Pour gérer le cast, dans vos sous-classes, vous pouvez fournir une méthode statique fromJSON () qui désérialise et effectue le cast approprié à partir de votre type d'interface. Cela a fonctionné à merveille pour moi (faites juste attention à la sérialisation / désérialisation des classes qui contiennent des hashmaps - ajoutez ceci lorsque vous instanciez votre générateur gson:

GsonBuilder builder = new GsonBuilder().enableComplexMapKeySerialization();

J'espère que cela aidera quelqu'un à gagner du temps et des efforts!

sodapoparooni
la source