Поліморфізм з гзоном


103

У мене є проблема десеріалізації рядка json з Gson. Я отримую масив команд. Команда може бути запуском, зупинкою, деяким іншим типом команди. Природно, що у мене поліморфізм, і команда start / stop успадковується від команди.

Як я можу серіалізувати його до правильного командного об'єкта за допомогою gson?

Здається, що я отримую лише базовий тип, тобто заявлений тип і ніколи тип виконання.


Відповіді:


120

Це трохи пізно, але мені довелося сьогодні робити саме те саме. Отже, виходячи з моїх досліджень і при використанні gson-2.0 ви дійсно не хочете використовувати метод registerTypeHierarchyAdapter , а навпаки більш мирський registerTypeAdapter . І вам, звичайно, не потрібно робити instanceofs або писати адаптери для похідних класів: лише один адаптер для базового класу або інтерфейсу, якщо, звичайно, ви задоволені серіалізацією похідних класів за замовчуванням. У всякому разі, ось код (пакет та імпорт видалено) (також доступний у github ):

Базовий клас (інтерфейс у моєму випадку):

public interface IAnimal { public String sound(); }

Два похідні класи, Кіт:

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

І Тестовий клас:

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 , але це, здавалося б, вимагає впроваджувати власні класи серіалізаторів / десяріалізаторів CatAdapter і CatAdapter, які є болем підтримувати будь-який час, коли ви хочете додати ще одне поле до собаки чи кота.


5
Зауважте, що серіалізація імен класів та десяриалізація (із введення користувача) за допомогою Class.forName може представляти наслідки для безпеки в деяких ситуаціях, і, таким чином, відлякує команда розробників Gson. code.google.com/p/google-gson/isissue/detail?id=340#c2
програміст Брюс

4
Як вам вдалося не отримати нескінченний цикл при серіалізації, ви викликаєте контекст.serialize (src); який знову викличе ваш адаптер. Це те, що трапилося в моєму подібному коді.
че явара

6
Неправильно. Це рішення не працює. Якщо ви закликаєте контекст.серіалізувати будь-яким способом, у вас виявиться нескінченна рекурсія. Цікаво, чому люди публікують без фактичного тестування коду. Я спробував з 2.2.1. Дивіться помилку , описану в stackoverflow.com/questions/13244769 / ...
че javara

4
@MarcusJuniusBrutus Я запустив ваш код і, здається, він працює лише в цьому спеціальному випадку - тому що ви визначили суперінтерфейс IAnimal, і IAnimalAdapter використовує його. Якщо ви замість цього мали лише "Cat", то у вас з’явиться нескінченна проблема рекурсії. Тож це рішення все ще не працює в загальному випадку - лише тоді, коли ви зможете визначити загальний інтерфейс. У моєму випадку не було інтерфейсу, тому мені довелося використовувати інший підхід із TypeAdapterFactory.
че явара

2
Користувач src.getClass (). GetName () замість src.getClass (). GetCanonicalName (). Це meads код буде працювати і для внутрішніх / вкладених класів.
mR_fr0g

13

В даний час у Gson є механізм реєстрації адаптера ієрархії типів, який, як повідомляється, може бути налаштований для простої поліморфної десеріалізації, але я не бачу, як це відбувається, оскільки адаптер ієрархії типу виявляється просто комбінованим серіалізатором / десеріалізатором / автором екземпляра, залишаючи деталі створення екземпляра до кодера, не надаючи фактичної реєстрації поліморфного типу.

Схоже, незабаром у Гсона з’явиться RuntimeTypeAdapterпростіша поліморфна десеріалізація. Докладнішу інформацію див. У розділі http://code.google.com/p/google-gson/isissue/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);
  }
}

Якщо ви можете змінити API, то зауважте, що в даний час у Джексона є механізм відносно простої поліморфної десеріалізації. Я розмістив кілька прикладів на programmerbruce.blogspot.com/2011/05/…
Програміст Брюс

RuntimeTypeAdapterНа сьогодні завершено, на жаль, схоже, що це не в ядрі Gson. :-(
Джонатан

8

Маркус Юній Брут мав чудову відповідь (спасибі!). Щоб розширити його приклад, ви можете зробити його клас адаптерів загальним для роботи для всіх типів об'єктів (Не тільки 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>());
        ....
}

1
Після втілення свого рішення моєю наступною думкою було зробити саме це :-)
Девід Леві

7

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

Метод серіалізації адаптера може бути каскадним, якщо інше перевірити, який тип він серіалізує.

    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;

Десеріалізація дещо хитра. У прикладі тестового модуля він перевіряє наявність атрибутів Tell-Tell, щоб вирішити, до якого класу слід деріаріалізувати. Якщо ви можете змінити джерело об'єкта, який ви серіалізуєте, ви можете додати атрибут 'classType' до кожного екземпляра, який містить FQN імені класу екземпляра. Це так дуже не орієнтовано на об'єкти.


4

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

Давно минуло, але я не зміг знайти справжнє хороше рішення в Інтернеті. Ось невеликий поворот щодо рішення @ 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();
  }
}

Причиною відсутності нескінченної рекурсії є те, що ми передаємо фактичний клас виконання (тобто собака не IAnimal) context.deserialize. Це не буде викликати наш тип адаптера, до тих пір , як ми використовуємо registerTypeAdapterі неregisterTypeHierarchyAdapter


2

Оновлений відповідь - найкращі частини всіх інших відповідей

Я описую рішення для різних випадків використання і також би вирішував нескінченну проблему рекурсії

  • Випадок 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}}

1

Якщо ви хочете керувати 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


0

Якщо ви поєднаєте відповідь Маркуса Юнія Брута з редагуванням user2242263, ви можете уникнути необхідності вказувати велику ієрархію класів у своєму адаптері, визначивши адаптер як тип інтерфейсу. Потім ви можете забезпечити реалізацію за замовчуванням toJSON () та fromJSON () у своєму інтерфейсі (який включає лише ці два методи) та мати кожен клас, необхідний для серіалізації для реалізації вашого інтерфейсу. Щоб вирішити кастинг, у своїх підкласах ви можете надати статичний метод fromJSON (), який десеріалізує та виконує відповідний кастинг від вашого типу інтерфейсу. Це чудово працювало для мене (просто будьте обережні щодо серіалізації / десеріалізації класів, які містять хешмапи - додайте це, коли ви інстанціюєте свого gson builder:

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

Сподіваюся, це допоможе комусь заощадити час та зусилля!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.