Полиморфизм с гсоном - PullRequest
       36

Полиморфизм с гсоном

91 голосов
/ 27 апреля 2011

У меня проблема с десериализацией строки json с помощью Gson. Я получаю массив команд. Командой может быть запуск, остановка, другой тип команды. Естественно, у меня есть полиморфизм, и команда start / stop наследуется от команды.

Как я могу сериализовать его обратно к правильному объекту команды, используя gson?

Кажется, что я получаю только базовый тип, то есть объявленный тип, а не тип времени выполнения.

Ответы [ 7 ]

112 голосов
/ 31 декабря 2011

Это немного поздно, но я должен был сделать то же самое сегодня.Итак, основываясь на моих исследованиях и при использовании gson-2.0 вы действительно не хотите использовать метод registerTypeHierarchyAdapter , а скорее более приземленный registerTypeAdapter .И вам, конечно, не нужно делать instanceofs или писать адаптеры для производных классов: только один адаптер для базового класса или интерфейса, при условии, конечно, что вы довольны сериализацией производных классов по умолчанию.В любом случае, вот код (пакет и импорт удалены) (также доступен в github ):

Базовый класс (интерфейс в моем случае):

public interface IAnimal { public String sound(); }

два производных класса, Cat:

public class Cat implements IAnimal {

    public String name;

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

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

и Dog:

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

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

И тестовый класс:

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

Когда вы запускаете Test :: main, вы получаете следующий вывод:

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)

Я на самом деле сделал вышеупомянутое, также используя метод registerTypeHierarchyAdapter , но это, казалось, требовалореализация пользовательских классов сериализатора / десериализатора DogAdapter и CatAdapter, которые трудно поддерживать в любое время, когда вы хотите добавить другое поле в Dog или Cat.

12 голосов
/ 08 июня 2011

Gson в настоящее время имеет механизм регистрации адаптера иерархии типов , который, как сообщается, может быть сконфигурирован для простой полиморфной десериализации, но я не вижу, как это так, поскольку адаптер иерархии типов выглядит просто объединенный создатель сериализатора / десериализатора / экземпляра, оставляющий детали создания экземпляра на усмотрение кодера, без предоставления какой-либо фактической регистрации полиморфного типа.

Похоже, у Gson скоро будет RuntimeTypeAdapter для более простой полиморфной десериализации. См. http://code.google.com/p/google-gson/issues/detail?id=231 для получения дополнительной информации.

Если использование нового RuntimeTypeAdapter невозможно, и вы должны использовать Gson, то я думаю, что вам придется развернуть собственное решение, зарегистрировав собственный десериализатор либо в качестве адаптера иерархии типов, либо в качестве адаптера типа. Ниже приведен один из таких примеров.

// 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);
  }
}
8 голосов
/ 11 апреля 2013

У Маркуса Юния Брута был отличный ответ (спасибо!).Чтобы расширить его пример, вы можете сделать его класс адаптера универсальным для работы со всеми типами объектов (не только IAnimal) со следующими изменениями:

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

А в классе тестирования:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}
6 голосов
/ 03 ноября 2011

GSON имеет довольно хороший тестовый пример, показывающий, как определить и зарегистрировать адаптер иерархии типов.

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

Чтобы использовать для этого:

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

Метод сериализации адаптера может быть каскадной проверкой if-else того типа, который он сериализует.

    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;

Десериализация немного хакерская.В примере модульного теста он проверяет наличие контрольных атрибутов, чтобы решить, в какой класс десериализоваться.Если вы можете изменить источник сериализуемого объекта, вы можете добавить атрибут «classType» к каждому экземпляру, который содержит FQN имени класса экземпляра.Хотя это очень не объектно-ориентировано.

3 голосов
/ 15 ноября 2017

Google выпустила собственную RuntimeTypeAdapterFactory для обработки полиморфизма, но, к сожалению, она не является частью ядра gson (вы должны скопировать и вставить класс внутри вашего проекта).

Пример:

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

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

Здесь Я разместил полный рабочий пример этого, используя модели Animal, Dog и Cat.

Я думаю, что лучше полагаться на этот адаптер, чем напереопределить его с нуля.

2 голосов
/ 22 октября 2015

Прошло много времени, но я не смог найти действительно хорошее решение онлайн .. Вот небольшой поворот в решении @ MarcusJuniusBrutus, позволяющий избежать бесконечной рекурсии.

Сохраните тот же десериализатор, но удалите сериализатор -

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

Затем в исходном классе добавьте поле с @SerializedName("CLASSNAME"). Хитрость заключается в том, чтобы инициализировать это в конструкторе базового класса , поэтому сделайте ваш интерфейс абстрактным.

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

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

Причина, по которой здесь нет бесконечной рекурсии, заключается в том, что мы передаем фактический класс времени выполнения (то есть Dog не IAnimal) в context.deserialize. Это не вызовет наш тип адаптера, если мы используем registerTypeAdapter, а не registerTypeHierarchyAdapter

1 голос
/ 23 ноября 2016

Если вы хотите управлять TypeAdapter для типа и другим для его подтипа, вы можете использовать TypeAdapterFactory следующим образом:

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

Эта фабрика отправит наиболее точный TypeAdapter

...