web-dev-qa-db-fra.com

Polymorphisme avec gson

J'ai un problème de désérialisation d'une chaîne json avec Gson. Je reçois un tableau 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 vers l'objet de commande correct à l'aide de gson?

Semble que je reçois uniquement le type de base, c'est-à-dire le type déclaré et jamais le type d'exécution.

93
Sophie

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 lorsque vous utilisez gson-2.0, vous ne voulez vraiment pas utiliser la méthode registerTypeHierarchyAdapter , mais plutôt la plus banale registerTypeAdapter . Et vous n'avez certainement pas besoin de faire instanceofs ou d'écrire des adaptateurs pour les classes dérivées: un seul adaptateur pour la classe de base ou l'interface, à condition bien sûr que vous êtes 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 en fait fait ce qui précède en utilisant la méthode registerTypeHierarchyAdapter également, mais cela semblait nécessiter la mise en œuvre de classes sérialiseur/désérialiseur DogAdapter et CatAdapter personnalisées, ce qui est difficile à maintenir à tout moment que vous souhaitez ajouter un autre champ à Dog ou à Cat.

114

Gson dispose actuellement d'un mécanisme pour enregistrer un adaptateur de hiérarchie de type qui aurait été configuré pour une désérialisation polymorphe simple, mais je ne vois pas comment c'est le cas, car un adaptateur de hiérarchie de type semble être simplement un combiné sérialiseur/désérialiseur/créateur d'instance, 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 du nouveau RuntimeTypeAdapter n'est pas possible et que vous devez utiliser Gson, alors je pense que vous devrez rouler votre propre solution, en enregistrant un désérialiseur personnalisé soit en tant qu'adaptateur de hiérarchie de type, soit 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);
  }
}
12
Programmer Bruce

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>());
        ....
}
8
user2242263

GSON a un assez 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 en cascade if-else du type de sérialisation.

    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 hacky. Dans l'exemple de test unitaire, il vérifie l'existence d'attributs révélateurs pour décider à 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. C'est tellement très non orienté objet.

6
k.c. sham

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 à l'aide des modèles Animal, Dog et Cat.

Je pense qu'il vaut mieux se fier à cet adaptateur plutôt que de le réimplémenter à partir de zéro.

3
db80

Le temps s'est écoulé depuis longtemps, 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écursion 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 à l'initialiser dans le constructeur de la classe de base, donc faites de votre interface 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écursion infinie ici est que nous transmettons 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 registerTypeAdapter et non registerTypeHierarchyAdapter

2
Ginandi

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 l'adaptateur de type le plus précis

1
r3n0j

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

Je décris des solutions pour différents cas d'utilisation et je traiterais également le problème récursion infinie

  • Cas 1: Vous contrôlez les classes , c'est-à-dire que vous pouvez écrire votre propre Cat, Dog ainsi que l'interface IAnimal. 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écursion infinie s'il existe une interface de base commune comme IAnimal

    Mais, que faire si je ne veux pas implémenter le IAnimal ou une telle interface?

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

    Il faudrait 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, quelle que soit la modification de la classe de base, votre wrapper continuera avec le même rôle. Deuxièmement, si nous ne définissons pas de constructeur de copie et que nous sous-classons simplement la classe de base, nous devons 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 - c'est l'implémentation RuntimeTypeAdapterFactory:

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 devez introduire un champ appelé "type" dans Animal et la même valeur à l'intérieur Dog pour être "dog", Cat pour être "cat"

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 marqueur 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 CatWrapper au lieu de Cat, DogWrapper au lieu de Dog et AlternativeAnimalAdapter au lieu de IAnimalAdapter

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

Sortie:

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}}
0
pulp_fiction