Polimorfismo con gson

103

Tengo un problema para deserializar una cadena json con Gson. Recibo una serie de comandos. El comando puede ser iniciar, detener, algún otro tipo de comando. Naturalmente, tengo polimorfismo y el comando de inicio / parada hereda del comando.

¿Cómo puedo serializarlo de nuevo en el objeto de comando correcto usando gson?

Parece que obtengo solo el tipo base, que es el tipo declarado y nunca el tipo de tiempo de ejecución.

Sophie
fuente

Respuestas:

120

Esto es un poco tarde, pero tuve que hacer exactamente lo mismo hoy. Entonces, según mi investigación y cuando use gson-2.0, realmente no desea usar el método registerTypeHierarchyAdapter , sino el más mundano registerTypeAdapter . Y ciertamente no necesita hacer instanceofs ni escribir adaptadores para las clases derivadas: solo un adaptador para la clase base o interfaz, siempre que, por supuesto, esté satisfecho con la serialización predeterminada de las clases derivadas. De todos modos, aquí está el código (paquete e importaciones eliminados) (también disponible en github ):

La clase base (interfaz en mi caso):

public interface IAnimal { public String sound(); }

Las dos clases derivadas, Cat:

public class Cat implements IAnimal {

    public String name;

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

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

Y perro:

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

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

Y la clase de prueba:

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

Cuando ejecuta Test :: main, obtiene el siguiente resultado:

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)

De hecho, también hice lo anterior usando el método registerTypeHierarchyAdapter , pero eso parecía requerir la implementación de clases personalizadas de serializador / deserializador de DogAdapter y CatAdapter, que son difíciles de mantener en cualquier momento que desee agregar otro campo a Dog o Cat.

Marco Junio ​​Bruto
fuente
5
Tenga en cuenta que la serialización de nombres de clases y la deserialización (a partir de la entrada del usuario) utilizando Class.forName puede presentar implicaciones de seguridad en algunas situaciones y, por lo tanto, el equipo de desarrollo de Gson lo desaconseja. code.google.com/p/google-gson/issues/detail?id=340#c2
Programador Bruce
4
¿Cómo se las arregló para no obtener un bucle infinito en la serialización? Está llamando a context.serialize (src); que invocará su adaptador nuevamente. Esto es lo que sucedió en mi código similar.
che javara
6
Incorrecto. Esta solución no funciona. Si llama a context.serialize de alguna manera, terminará con una recursividad infinita. Me pregunto por qué la gente publica sin probar el código. Probé con 2.2.1. Vea el error descrito en stackoverflow.com/questions/13244769/…
che javara
4
@MarcusJuniusBrutus Ejecuté su código y parece que solo funciona en este caso especial, porque ha definido una superinterfaz IAnimal, y IAnimalAdapter la usa. Si, en cambio, solo tenía 'Cat', obtendrá el problema de recursividad infinita. Por lo tanto, esta solución todavía no funciona en el caso general, solo cuando puede definir una interfaz común. En mi caso, no había interfaz, así que tuve que usar un enfoque diferente con TypeAdapterFactory.
che javara
2
Usuario src.getClass (). GetName () en lugar de src.getClass (). GetCanonicalName (). Esto significa que el código también funcionará para clases internas / anidadas.
mR_fr0g
13

Gson tiene actualmente un mecanismo para registrar un adaptador de jerarquía de tipos que, según se informa, se puede configurar para una deserialización polimórfica simple, pero no veo cómo ese es el caso, ya que un Adaptador de jerarquía de tipos parece ser solo un serializador / deserializador / creador de instancias combinado, dejando los detalles de la creación de la instancia en manos del codificador, sin proporcionar ningún registro de tipo polimórfico real.

Parece que Gson pronto tendrá la RuntimeTypeAdapterdeserialización polimórfica más simple. Ver http://code.google.com/p/google-gson/issues/detail?id=231 para obtener más información.

Si RuntimeTypeAdapterno es posible usar el nuevo y debe usar Gson, entonces creo que tendrá que lanzar su propia solución, registrando un deserializador personalizado como Adaptador de jerarquía de tipos o como Adaptador de tipos. A continuación se muestra uno de esos ejemplos.

// 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);
  }
}
Programador Bruce
fuente
Si puede cambiar las API, tenga en cuenta que Jackson tiene actualmente un mecanismo para la deserialización polimórfica relativamente simple. Publiqué algunos ejemplos en programmerbruce.blogspot.com/2011/05/…
Programador Bruce
RuntimeTypeAdapterahora está completo, desafortunadamente no parece que esté en el núcleo de Gson todavía. :-(
Jonathan
8

Marcus Junius Brutus tuvo una gran respuesta (¡gracias!). Para ampliar su ejemplo, puede hacer que su clase de adaptador sea genérica para que funcione para todo tipo de objetos (no solo IAnimal) con los siguientes cambios:

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

Y en la clase de prueba:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}
usuario2242263
fuente
1
Después de implementar su solución, mi siguiente pensamiento fue hacer exactamente esto :-)
David Levy
7

GSON tiene un caso de prueba bastante bueno aquí que muestra cómo definir y registrar un adaptador de jerarquía de tipos.

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

Para usar eso, haz esto:

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

El método de serialización del adaptador puede ser una verificación en cascada si-si no del tipo que está serializando.

    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;

Deserializar es un poco hacky. En el ejemplo de prueba unitaria, verifica la existencia de atributos reveladores para decidir a qué clase deserializar. Si puede cambiar la fuente del objeto que está serializando, puede agregar un atributo 'classType' a cada instancia que contiene el FQN del nombre de la clase de instancia. Sin embargo, esto es muy poco orientado a objetos.

kc farsa
fuente
4

Google ha lanzado su propio RuntimeTypeAdapterFactory para manejar el polimorfismo, pero desafortunadamente no es parte del núcleo gson (debe copiar y pegar la clase dentro de su proyecto).

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í he publicado un ejemplo completo de trabajo usando los modelos Animal, Dog y Cat.

Creo que es mejor confiar en este adaptador en lugar de volver a implementarlo desde cero.

db80
fuente
2

Ha pasado mucho tiempo, pero no pude encontrar una solución realmente buena en línea. Aquí hay un pequeño giro en la solución de @ MarcusJuniusBrutus, que evita la recursividad infinita.

Mantenga el mismo deserializador, pero elimine el serializador -

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

Luego, en su clase original, agregue un campo con @SerializedName("CLASSNAME"). El truco ahora es inicializar esto en el constructor de la clase base , así que convierta su interfaz en una clase abstracta.

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

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

La razón por la que no hay una recursividad infinita aquí es que pasamos la clase de tiempo de ejecución real (es decir, Perro, no IAnimal) a context.deserialize. Esto no llamará a nuestro adaptador de tipo, siempre que usemos registerTypeAdaptery noregisterTypeHierarchyAdapter

Ginandi
fuente
2

Respuesta actualizada: las mejores partes de todas las demás respuestas

Estoy describiendo soluciones para diferentes casos de uso y se dirigiría a la recursividad infinita problema también

  • Caso 1: Usted está en control de las clases , es decir, se llega a escribir sus propios Cat, Dogclases, así como laIAnimal interfaz. Simplemente puede seguir la solución proporcionada por @ marcus-junius-brutus (la respuesta mejor calificada)

    No habrá ninguna recursividad infinita si hay una interfaz base común como IAnimal

    Pero, ¿qué pasa si no quiero implementar la IAnimalinterfaz o ninguna de ellas?

    Entonces, @ marcus-junius-brutus (la respuesta mejor calificada) producirá un error de recursividad infinito. En este caso, podemos hacer algo como a continuación.

    Tendríamos que crear un constructor de copia dentro de la clase base y una subclase contenedora de la siguiente manera:

.

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

Y el serializador para el 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;
    }
}

Entonces, ¿por qué un constructor de copias?

Bueno, una vez que defina el constructor de copia, no importa cuánto cambie la clase base, su envoltorio continuará con el mismo rol. En segundo lugar, si no definimos un constructor de copia y simplemente subclasificamos la clase base, entonces tendríamos que "hablar" en términos de la clase extendida, es decir,CatWrapper . Es muy posible que sus componentes hablen en términos de la clase base y no del tipo de contenedor.

¿Existe una alternativa fácil?

Claro, ahora ha sido introducido por Google; esta es la RuntimeTypeAdapterFactoryimplementación:

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

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

Aquí, deberías introducir un campo llamado "tipo" Animaly el valor del mismo dentro Dogpara ser "perro", Catpara ser "gato".

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

  • Caso 2: No tienes el control de las clases . Te unes a una empresa o usas una biblioteca donde las clases ya están definidas y tu gerente no quiere que las cambies de ninguna manera: puedes subclasificar tus clases y hacer que implementen una interfaz de marcador común (que no tiene ningún método ) como 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);
    }
}

Entonces, estaríamos usando en CatWrapperlugar de Cat, en DogWrapperlugar de Dogy en AlternativeAnimalAdapterlugar 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);
    }
}

Realizamos una prueba:

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

Salida:

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
fuente
1

Si desea administrar un TypeAdapter para un tipo y otro para su subtipo, puede usar un TypeAdapterFactory como este:

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

Esta fábrica enviará el TypeAdapter más preciso

r3n0j
fuente
0

Si combina la respuesta de Marcus Junius Brutus con la edición de user2242263, puede evitar tener que especificar una jerarquía de clases grande en su adaptador definiendo que su adaptador funciona en un tipo de interfaz. Luego puede proporcionar implementaciones predeterminadas de toJSON () y fromJSON () en su interfaz (que solo incluye estos dos métodos) y hacer que todas las clases que necesita para serializar implementen su interfaz. Para lidiar con la conversión, en sus subclases puede proporcionar un método fromJSON () estático que deserializa y realiza la conversión adecuada desde su tipo de interfaz. Esto funcionó de maravilla para mí (solo tenga cuidado con la serialización / deserialización de clases que contienen hashmaps; agregue esto cuando cree una instancia de su generador de gson:

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

¡Espero que esto ayude a alguien a ahorrar tiempo y esfuerzo!

sodapoparooni
fuente