Полиморфизм с gson
У меня проблема десериализации json-строки с Gson.
Я получаю массив команд. Командой может быть запуск, остановка, другой тип команды. Естественно, у меня есть полиморфизм, а команда start/stop наследуется от команды.
Как преобразовать его в правильный объект команды с помощью gson?
Кажется, что я получаю только базовый тип, который является объявленным типом и никогда не является типом времени выполнения.
Ответы
Ответ 1
Это немного поздно, но сегодня я должен был сделать то же самое. Итак, основываясь на моих исследованиях и при использовании 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\"";
};
}
И собака:
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);
}
}
И класс Test:
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, которые являются болью для поддержания любого времени, которое вы хотите добавить поле для собаки или кошки.
Ответ 2
В настоящее время у 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);
}
}
Ответ 3
У Маркуса Юния Брута был отличный ответ (спасибо!). Чтобы расширить его пример, вы можете сделать его класс адаптера универсальным для работы для всех типов объектов (не только 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>());
....
}
Ответ 4
В 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;
Deserializing немного взломан. В примере unit test он проверяет наличие атрибутов контрола, чтобы решить, к какому классу десериализован. Если вы можете изменить источник сериализуемого объекта, вы можете добавить атрибут classType для каждого экземпляра, который содержит FQN имени класса экземпляра. Это так очень не объектно-ориентированное.
Ответ 5
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.
Я думаю, что лучше полагаться на этот адаптер, а не переопределять его с нуля.
Ответ 6
Долгое время прошло, но я не смог найти действительно хорошее решение в Интернете..
Вот небольшой поворот в решении @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 not IAnimal) на context.deserialize
. Это не будет называть наш адаптер типа, если мы используем registerTypeAdapter
, а не registerTypeHierarchyAdapter
Ответ 7
Если вы хотите управлять 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;
}
}
Этот factory отправит самый точный TypeAdapter
Ответ 8
Обновленный ответ - лучшие части всех остальных ответов
Я описываю решения для различных случаев использования, а также буду решать проблему бесконечной рекурсии
Случай 1: Вы управляете классами, то есть вы можете написать свои собственные классы Cat
, Dog
, а также интерфейс IAnimal
. Вы можете просто следовать решению, предоставленному @marcus-junius-brutus (самый рейтинговый ответ)
Не будет никакой бесконечной рекурсии, если есть общий базовый интерфейс как IAnimal
Но что, если я не хочу реализовывать IAnimal
или какой-либо подобный интерфейс?
Тогда @marcus-junius-brutus (самый рейтинговый ответ) выдаст бесконечную ошибку рекурсии. В этом случае мы можем сделать что-то вроде ниже.
Нам нужно создать конструктор копирования внутри базового класса и подкласса оболочки следующим образом:
.
// 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);
}
}
И сериализатор для типа 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;
}
}
Итак, почему конструктор копирования?
Что ж, после того, как вы определите конструктор копирования, независимо от того, насколько сильно изменится базовый класс, ваша оболочка будет продолжать выполнять ту же роль. Во-вторых, если мы не определим конструктор копирования и просто создадим подкласс базового класса, то нам придется "говорить" в терминах расширенного класса, то есть CatWrapper
. Вполне возможно, что ваши компоненты говорят в терминах базового класса, а не типа оболочки.
Есть ли легкая альтернатива?
Конечно, он был представлен Google - это реализация RuntimeTypeAdapterFactory
:
RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(runtimeTypeAdapterFactory)
.create();
Здесь вам нужно будет ввести поле с именем "type" в Animal
и его значение внутри Dog
, которое будет "dog", Cat
, чтобы быть "cat"
Полный пример: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html
Случай 2: Вы не контролируете классы. Вы присоединяетесь к компании или используете библиотеку, в которой классы уже определены, и ваш менеджер не хочет, чтобы вы их каким-либо образом изменили - вы можете создать подклассы для ваших классов и заставить их реализовать общий интерфейс маркеров (у которого нет никаких методов), например как AnimalInterface
.
Пример:
.
// 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);
}
}
Итак, мы будем использовать CatWrapper
вместо Cat
, DogWrapper
вместо Dog
и
AlternativeAnimalAdapter
вместо 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);
}
}
Мы проводим тест:
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);
}
}
}
Выход:
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}}
Ответ 9
Если вы объедините ответ Marcus Junius Brutus с правкой user2242263, вы можете избежать необходимости указывать большую иерархию классов в своем адаптере, определяя свой адаптер как работающий с типом интерфейса. Затем вы можете предоставить реализации по умолчанию toJSON() и fromJSON() в своем интерфейсе (который включает только эти два метода) и иметь каждый класс, необходимый для сериализации, реализующий ваш интерфейс. Чтобы иметь дело с приведением типов, в ваших подклассах вы можете предоставить статический метод fromJSON(), который десериализует и выполняет соответствующее приведение типов вашего интерфейса. Это отлично сработало для меня (просто будьте осторожны с сериализацией/десериализацией классов, которые содержат хеш-карты - добавьте это, когда вы создаете экземпляр своего сборщика gson:
GsonBuilder builder = new GsonBuilder().enableComplexMapKeySerialization();
Надеюсь, это поможет кому-то сэкономить время и силы!