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

2voto

Ginandi Points 82

Un long moment s'est écoulé, mais je n'ai pas trouvé de solution vraiment bonne en ligne Voici une petite variante de la solution de @MarcusJuniusBrutus, qui évite la récursion infinie.

Conservez 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 dont le nom est @SerializedName("CLASSNAME") . L'astuce est maintenant d'initialiser ceci dans le constructeur de la classe de base Vous pouvez donc transformer 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écursion infinie ici est que nous passons la classe d'exécution réelle (c'est-à-dire Dog et non IAnimal) à context.deserialize . Ceci n'appellera pas notre adaptateur de type, tant que nous utilisons registerTypeAdapter et non registerTypeHierarchyAdapter

2voto

pulp_fiction Points 10

Réponse actualisée - Meilleures parties de toutes les autres réponses

Je décris des solutions pour différents cas d'utilisation et j'aborderai les questions suivantes récursion infinie problème également

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

    Mais, que se passe-t-il si je ne veux pas implémenter le IAnimal ou toute autre interface de ce type ?

    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.

    Nous devrions créer un constructeur de copie à l'intérieur de la classe de base et d'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 définissez 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 de constructeur de copie et que nous nous contentons de sous-classer la classe de base, nous devrons "parler" en termes de classe étendue, à savoir CatWrapper . Il est tout à fait possible que vos composants parlent en termes de classe de base et non de type de wrapper.

Existe-t-il une alternative facile ?

Bien sûr, elle a maintenant été introduite par Google - c'est la RuntimeTypeAdapterFactory mise 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();

Dans ce cas, vous devez introduire un champ appelé "type" dans la base de données de l'entreprise. Animal et la valeur de la même à l'intérieur Dog pour être "chien", Cat pour ê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 cours . Vous rejoignez une entreprise ou utilisez une bibliothèque où les classes sont déjà définies et votre responsable ne veut pas que vous les changiez de quelque façon que ce soit - Vous pouvez sous-classer vos classes et leur faire implémenter une interface de marquage commune (qui n'a pas de méthodes) telle que 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 y 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}}

1voto

r3n0j Points 71

Si vous voulez 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.

0voto

sodapoparooni Points 11

Si vous combinez la réponse de Marcus Junius Brutus avec la modification de user2242263, vous pouvez éviter de devoir 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 alors fournir des implémentations par défaut de toJSON() et fromJSON() dans votre interface (qui ne comprend 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 casting, dans vos sous-classes, vous pouvez fournir une méthode statique fromJSON() qui désérialise et effectue le casting approprié à partir du type de votre interface. Cela a très bien fonctionné pour moi (faites juste attention à la sérialisation/désérialisation des classes qui contiennent des hashmaps - ajoutez ceci lorsque vous instanciez votre constructeur gson :

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

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

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