Polimorfismo con gson


103

Ho un problema durante la deserializzazione di una stringa JSON con Gson. Ricevo una serie di comandi. Il comando può essere avvio, arresto, un altro tipo di comando. Naturalmente ho il polimorfismo e il comando di avvio / arresto eredita dal comando.

Come posso serializzarlo di nuovo nell'oggetto comando corretto usando gson?

Sembra che ottenga solo il tipo di base, ovvero il tipo dichiarato e mai il tipo di runtime.


Risposte:


120

È un po 'tardi, ma oggi ho dovuto fare esattamente la stessa cosa. Quindi, in base alla mia ricerca e quando si utilizza gson-2.0, non si desidera davvero utilizzare il metodo registerTypeHierarchyAdapter , ma piuttosto il più banale registerTypeAdapter . E di certo non è necessario fare instanceofs o scrivere adattatori per le classi derivate: solo un adattatore per la classe base o l'interfaccia, a condizione ovviamente che tu sia soddisfatto della serializzazione predefinita delle classi derivate. Ad ogni modo, ecco il codice (pacchetto e importazioni rimossi) (disponibile anche in GitHub ):

La classe base (interfaccia nel mio caso):

public interface IAnimal { public String sound(); }

Le due classi derivate, Cat:

public class Cat implements IAnimal {

    public String name;

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

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

E cane:

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

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

E la classe di prova:

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

Quando esegui Test :: main ottieni il seguente output:

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)

In realtà ho fatto quanto sopra utilizzando anche il metodo registerTypeHierarchyAdapter , ma sembrava che ciò richiedesse l'implementazione di classi di serializzatore / deserializzatore DogAdapter e CatAdapter personalizzate che sono un problema da mantenere ogni volta che si desidera aggiungere un altro campo a Dog o Cat.


5
Si noti che la serializzazione dei nomi delle classi e la deserializzazione (dall'input dell'utente) utilizzando Class.forName possono presentare implicazioni sulla sicurezza in alcune situazioni ed è quindi scoraggiata dal team di sviluppo di Gson. code.google.com/p/google-gson/issues/detail?id=340#c2
Programmer Bruce

4
Come sei riuscito a non ottenere un ciclo infinito nella serializzazione, stai chiamando context.serialize (src); che richiamerà nuovamente l'adattatore. Questo è quello che è successo nel mio codice simile.
che javara

6
Sbagliato. Questa soluzione non funziona. Se chiami context.serialize in qualsiasi modo, finirai con la ricorsione infinita. Mi chiedo perché le persone pubblicano senza testare effettivamente il codice. Ho provato con 2.2.1. Vedere il bug descritto nel stackoverflow.com/questions/13244769/...
Che JavaRa

4
@MarcusJuniusBrutus Ho eseguito il tuo codice e sembra che funzioni solo in questo caso speciale, perché hai definito una superinterfaccia IAnimal e IAnimalAdapter la usa. Se invece hai solo "Gatto", otterrai il problema della ricorsione infinita. Quindi questa soluzione ancora non funziona nel caso generale, solo quando sei in grado di definire un'interfaccia comune. Nel mio caso non c'era l'interfaccia, quindi ho dovuto utilizzare un approccio diverso con TypeAdapterFactory.
che javara

2
Utente src.getClass (). GetName () invece di src.getClass (). GetCanonicalName (). Questo significa che il codice funzionerà anche per le classi interne / annidate.
mR_fr0g

13

Gson attualmente dispone di un meccanismo per registrare un adattatore della gerarchia dei tipi che secondo quanto riferito può essere configurato per una semplice deserializzazione polimorfica, ma non vedo come sia il caso, poiché un adattatore della gerarchia dei tipi sembra essere solo un serializzatore / deserializzatore / creatore di istanze combinato, lasciando i dettagli della creazione dell'istanza al programmatore, senza fornire alcuna effettiva registrazione di tipo polimorfico.

Sembra che Gson avrà presto la RuntimeTypeAdapterdeserializzazione polimorfica più semplice. Vedi http://code.google.com/p/google-gson/issues/detail?id=231 per ulteriori informazioni.

Se l'uso del nuovo RuntimeTypeAdapternon è possibile e devi usare Gson, penso che dovrai eseguire la tua soluzione, registrando un deserializzatore personalizzato come adattatore di gerarchia di tipi o adattatore di tipo. Di seguito è riportato uno di questi esempi.

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

Se puoi modificare le API, tieni presente che Jackson attualmente ha un meccanismo per la deserializzazione polimorfica relativamente semplice. Ho pubblicato alcuni esempi su programmerbruce.blogspot.com/2011/05/…
Programmer Bruce

RuntimeTypeAdapterè ora completo, sfortunatamente non sembra ancora nel core di Gson. :-(
Jonathan

8

Marcus Junius Brutus ha avuto un'ottima risposta (grazie!). Per estendere il suo esempio, puoi rendere la sua classe dell'adattatore generica per funzionare per tutti i tipi di oggetti (non solo IAnimal) con le seguenti modifiche:

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
....
}

E nella classe di prova:

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

1
Dopo aver implementato la sua soluzione, il mio pensiero successivo è stato quello di fare esattamente questo :-)
David Levy

7

GSON ha un buon test case qui che mostra come definire e registrare un adattatore di gerarchia di tipi.

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

Per usarlo fai questo:

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

Il metodo di serializzazione dell'adattatore può essere un controllo if-else a cascata del tipo che sta serializzando.

    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 deserializzazione è un po 'hacky. Nell'esempio di unit test, verifica l'esistenza di attributi rivelatori per decidere in quale classe deserializzare. Se puoi modificare l'origine dell'oggetto che stai serializzando, puoi aggiungere un attributo "classType" a ciascuna istanza che contiene l'FQN del nome della classe dell'istanza. Questo però è molto poco orientato agli oggetti.


4

Google ha rilasciato il proprio RuntimeTypeAdapterFactory per gestire il polimorfismo ma sfortunatamente non fa parte del core gson (devi copiare e incollare la classe all'interno del tuo progetto).

Esempio:

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

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

Di seguito ne ho pubblicato un esempio funzionante utilizzando i modelli Animal, Dog e Cat.

Penso che sia meglio fare affidamento su questo adattatore piuttosto che reimplementarlo da zero.


2

È passato molto tempo, ma non sono riuscito a trovare una buona soluzione online .. Ecco una piccola svolta nella soluzione di @ MarcusJuniusBrutus, che evita la ricorsione infinita.

Mantieni lo stesso deserializzatore, ma rimuovi il serializzatore -

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

Quindi, nella tua classe originale, aggiungi un campo con @SerializedName("CLASSNAME"). Il trucco ora è inizializzarlo nel costruttore della classe base , quindi trasforma la tua interfaccia in una classe astratta.

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

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

Il motivo per cui non c'è ricorsione infinita qui è che passiamo la classe di runtime effettiva (cioè Dog non IAnimal) a context.deserialize. Questo non chiamerà il nostro adattatore di tipo, fintanto che usiamo registerTypeAdaptere nonregisterTypeHierarchyAdapter


2

Risposta aggiornata - Parti migliori di tutte le altre risposte

Sto descrivendo soluzioni per i vari casi di utilizzo e sarebbe rivolgo la ricorsione infinita problema pure

  • Caso 1: Siete nel controllo delle classi , vale a dire, si arriva a scrivere i propri Cat, Dogle classi, nonché laIAnimal interfaccia. Puoi semplicemente seguire la soluzione fornita da @ marcus-junius-brutus (la risposta più votata)

    Non ci sarà alcuna ricorsione infinita se esiste un'interfaccia di base comune come IAnimal

    Ma cosa succede se non voglio implementare l' IAnimalinterfaccia o una di queste interfacce?

    Quindi, @ marcus-junius-brutus (la risposta più votata) produrrà un errore di ricorsione infinito. In questo caso, possiamo fare qualcosa come di seguito.

    Dovremmo creare un costruttore di copia all'interno della classe base e una sottoclasse wrapper come segue:

.

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

E il serializzatore per il tipo 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;
    }
}

Allora, perché un costruttore di copie?

Bene, una volta definito il costruttore di copia, indipendentemente da quanto cambia la classe di base, il wrapper continuerà con lo stesso ruolo. In secondo luogo, se non definiamo un costruttore di copia e semplicemente sottoclassiamo la classe base, dovremmo "parlare" in termini di classe estesa, cioèCatWrapper . È del tutto possibile che i tuoi componenti parlino in termini di classe base e non di tipo wrapper.

C'è un'alternativa facile?

Certo, ora è stato introdotto da Google: questa è l' RuntimeTypeAdapterFactoryimplementazione:

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

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

Qui, dovresti introdurre un campo chiamato "tipo" in Animale il valore dello stesso all'interno Dogdeve essere "cane", Catessere "gatto"

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

  • Caso 2: non hai il controllo delle classi . Ti unisci a un'azienda o usi una libreria in cui le classi sono già definite e il tuo manager non vuole che tu le modifichi in alcun modo - Puoi sottoclassare le tue classi e fare in modo che implementino un'interfaccia marker comune (che non ha alcun metodo ) come 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);
    }
}

Quindi, useremmo CatWrapperinvece di Cat, DogWrapperinvece di Doge AlternativeAnimalAdapterinvece diIAnimalAdapter

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

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

Produzione:

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

1

Se vuoi gestire un TypeAdapter per un tipo e un altro per il suo sottotipo, puoi usare un TypeAdapterFactory come questo:

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

Questa fabbrica invierà il TypeAdapter più accurato


0

Se combini la risposta di Marcus Junius Brutus con la modifica di user2242263, puoi evitare di dover specificare una grande gerarchia di classi nell'adattatore definendo l'adattatore come funzionante su un tipo di interfaccia. È quindi possibile fornire implementazioni predefinite di toJSON () e fromJSON () nella propria interfaccia (che include solo questi due metodi) e disporre di ogni classe necessaria per serializzare l'implementazione dell'interfaccia. Per gestire il casting, nelle tue sottoclassi puoi fornire un metodo fromJSON () statico che deserializza ed esegue il casting appropriato dal tuo tipo di interfaccia. Questo ha funzionato in modo superbo per me (fai solo attenzione alla serializzazione / deserializzazione delle classi che contengono hashmap - aggiungi questo quando installi il tuo gson builder:

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

Spero che questo aiuti qualcuno a risparmiare tempo e fatica!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.