Polimorfismo com gson

103

Eu tenho um problema ao desserializar uma string json com Gson. Eu recebo uma série de comandos. O comando pode ser iniciar, parar ou algum outro tipo de comando. Naturalmente, tenho polimorfismo, e o comando iniciar / parar herda do comando.

Como posso serializá-lo de volta para o objeto de comando correto usando gson?

Parece que obtenho apenas o tipo base, que é o tipo declarado e nunca o tipo de tempo de execução.

Sophie
fonte

Respostas:

120

É um pouco tarde, mas tive que fazer exatamente a mesma coisa hoje. Então, com base em minha pesquisa e ao usar gson-2.0, você realmente não quer usar o método registerTypeHierarchyAdapter , mas o mais mundano registerTypeAdapter . E você certamente não precisa fazer instanceofs ou escrever adaptadores para as classes derivadas: apenas um adaptador para a classe base ou interface, desde que você esteja satisfeito com a serialização padrão das classes derivadas. De qualquer forma, aqui está o código (pacote e importações removidos) (também disponível no github ):

A classe base (interface no meu caso):

public interface IAnimal { public String sound(); }

As duas classes 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\"";
    };
}

E Cachorro:

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

O 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 a classe de teste:

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

Ao executar o Test :: main, você obtém a seguinte saída:

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)

Na verdade, fiz o acima usando o método registerTypeHierarchyAdapter também, mas isso parecia exigir a implementação de classes serializador / desserializador DogAdapter e CatAdapter, cuja manutenção é difícil sempre que você quiser adicionar outro campo a Dog ou Cat.

Marcus Junius Brutus
fonte
5
Observe que a serialização de nomes de classes e a desserialização (da entrada do usuário) usando Class.forName podem apresentar implicações de segurança em algumas situações e, portanto, é desencorajada pela equipe de desenvolvimento Gson. code.google.com/p/google-gson/issues/detail?id=340#c2
Programador Bruce
4
Como você conseguiu não obter um loop infinito na serialização, você está chamando context.serialize (src); que invocará seu adaptador novamente. Isso é o que aconteceu em meu código semelhante.
che javara
6
Errado. Esta solução não funciona. Se você chamar context.serialize de qualquer maneira, você terminará com recursão infinita. Eu me pergunto por que as pessoas postam sem realmente testar o código. Tentei com 2.2.1. Veja o bug descrito em stackoverflow.com/questions/13244769/…
che javara
4
@MarcusJuniusBrutus Eu executei seu código e parece que funciona apenas neste caso especial - porque você definiu uma superinterface IAnimal e IAnimalAdapter a usa. Se, em vez disso, você tivesse apenas 'Cat', obterá o problema de recursão infinita. Portanto, esta solução ainda não funciona no caso geral - apenas quando você é capaz de definir uma interface comum. No meu caso, não havia interface, então tive que usar uma abordagem diferente com TypeAdapterFactory.
che javara
2
Usuário src.getClass (). GetName () em vez de src.getClass (). GetCanonicalName (). Isso significa que o código funcionará para classes internas / aninhadas também.
mR_fr0g
13

Gson atualmente tem um mecanismo para registrar um adaptador de hierarquia de tipo que supostamente pode ser configurado para desserialização polimórfica simples, mas não vejo como é o caso, já que um adaptador de hierarquia de tipo parece ser apenas um serializador / desserializador / criador de instância combinado, deixando os detalhes da criação da instância para o codificador, sem fornecer nenhum registro de tipo polimórfico real.

Parece que em breve o Gson terá a RuntimeTypeAdapterdesserialização polimórfica mais simples. Consulte http://code.google.com/p/google-gson/issues/detail?id=231 para obter mais informações.

Se o uso do novo RuntimeTypeAdapternão for possível e você tiver que usar Gson, então acho que você terá que lançar sua própria solução, registrando um desserializador customizado como um Type Hierarchy Adapter ou como Type Adapter. A seguir está um exemplo.

// 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
fonte
Se você pode alterar APIs, então observe que Jackson atualmente tem um mecanismo para desserialização polimórfica relativamente simples. Postei alguns exemplos em programmerbruce.blogspot.com/2011/05/…
Programador Bruce
RuntimeTypeAdapteragora está completo, infelizmente ainda não parece estar no núcleo Gson. :-(
Jonathan
8

Marcus Junius Brutus teve uma ótima resposta (obrigado!). Para estender seu exemplo, você pode tornar sua classe de adaptador genérica para funcionar com todos os tipos de objetos (não apenas IAnimal) com as seguintes alterações:

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 na classe de teste:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}
user2242263
fonte
1
Depois de implementar sua solução, meu próximo pensamento foi fazer exatamente isso :-)
David Levy
7

GSON tem um caso de teste muito bom aqui, mostrando como definir e registrar um adaptador de hierarquia de tipo.

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

Para usar isso, faça o seguinte:

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

O método de serialização do adaptador pode ser uma verificação if-else em cascata de que tipo ele 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;

A desserialização é um pouco hackeada. No exemplo de teste de unidade, ele verifica a existência de atributos indicadores para decidir para qual classe será desserializado. Se você pode alterar a fonte do objeto que está serializando, pode adicionar um atributo 'classType' a cada instância que contém o FQN do nome da classe de instância. Porém, isso é muito não orientado a objetos.

kc sham
fonte
4

O Google lançou seu próprio RuntimeTypeAdapterFactory para lidar com o polimorfismo, mas infelizmente ele não faz parte do núcleo gson (você deve copiar e colar a classe dentro do seu projeto).

Exemplo:

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

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

Aqui eu postei um exemplo completo de trabalho usando os modelos Animal, Dog e Cat.

Eu acho que é melhor confiar neste adaptador em vez de reimplementá-lo do zero.

db80
fonte
2

Muito tempo se passou, mas não consegui encontrar uma solução realmente boa online .. Aqui está uma pequena variação na solução de @MarcusJuniusBrutus, que evita a recursão infinita.

Mantenha o mesmo desserializador, mas remova o 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);
  }
}

Então, em sua classe original, adicione um campo com @SerializedName("CLASSNAME"). O truque agora é inicializar isso no construtor da classe base , portanto, transforme sua interface em uma classe abstrata.

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

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

A razão de não haver recursão infinita aqui é que passamos a classe de tempo de execução real (ou seja, Dog não IAnimal) para context.deserialize. Isso não chamará nosso adaptador de tipo, contanto que usemos registerTypeAdaptere nãoregisterTypeHierarchyAdapter

Ginandi
fonte
2

Resposta atualizada - melhores partes de todas as outras respostas

Estou descrevendo soluções para vários casos de uso e também abordaria o problema de recursão infinita

  • Caso 1: Você está no controle das classes , ou seja, você começa a escrever seus próprios Cat, Dogclasses, bem como a IAnimalinterface. Você pode simplesmente seguir a solução fornecida por @ marcus-junius-brutus (a resposta com melhor classificação)

    Não haverá recursão infinita se houver uma interface de base comum como IAnimal

    Mas, e se eu não quiser implementar a IAnimalou qualquer outra interface?

    Então, @ marcus-junius-brutus (a resposta mais bem avaliada) produzirá um erro de recursão infinito. Nesse caso, podemos fazer algo como a seguir.

    Teríamos que criar um construtor de cópia dentro da classe base e uma subclasse de wrapper da seguinte maneira:

.

// 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 o serializador para o 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;
    }
}

Então, por que um construtor de cópia?

Bem, uma vez que você defina o construtor de cópia, não importa o quanto a classe base mude, seu wrapper continuará com a mesma função. Em segundo lugar, se não definirmos um construtor de cópia e simplesmente criarmos uma subclasse da classe base, teremos que "conversar" em termos da classe estendida, ou seja CatWrapper,. É bem possível que seus componentes falem em termos da classe base e não do tipo de invólucro.

Existe uma alternativa fácil?

Claro, agora foi introduzido pelo Google - esta é a RuntimeTypeAdapterFactoryimplementação:

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

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

Aqui, você precisaria inserir um campo chamado "digite" Animale o valor do mesmo dentro Dogde ser "cachorro", Catser "gato"

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

  • Caso 2: Você não está no controle das aulas . Você entra para uma empresa ou usa uma biblioteca onde as classes já estão definidas e seu gerente não quer que você as altere de nenhuma forma - Você pode criar uma subclasse de suas classes e fazer com que implementem uma interface de marcador comum (que não tem métodos ) 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);
    }
}

Então, estaríamos usando em CatWrappervez de Cat, em DogWrappervez de Doge em AlternativeAnimalAdaptervez 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 um teste:

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

Resultado:

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

Se quiser gerenciar um TypeAdapter para um tipo e outro para seu subtipo, você pode usar um 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á o TypeAdapter mais preciso

r3n0j
fonte
0

Se você combinar a resposta de Marcus Junius Brutus com a edição do usuário 2242263, poderá evitar a necessidade de especificar uma grande hierarquia de classes em seu adaptador, definindo seu adaptador como trabalhando em um tipo de interface. Você pode então fornecer implementações padrão de toJSON () e fromJSON () em sua interface (que inclui apenas esses dois métodos) e ter todas as classes necessárias para serializar implementar sua interface. Para lidar com a conversão, em suas subclasses você pode fornecer um método estático fromJSON () que desserializa e executa a conversão apropriada do seu tipo de interface. Isso funcionou muito bem para mim (apenas tome cuidado ao serializar / desserializar classes que contêm hashmaps - adicione isso ao instanciar seu construtor gson:

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

Espero que isso ajude alguém a economizar tempo e esforço!

sodapoparooni
fonte