gsonによる多態性


103

Gsonでjson文字列をデシリアライズする際に問題が発生しました。コマンドの配列を受け取ります。コマンドには、start、stop、その他のタイプのコマンドがあります。当然私はポリモーフィズムを持っています、そして開始/停止コマンドはコマンドから継承します。

gsonを使用して正しいコマンドオブジェクトにシリアル化するにはどうすればよいですか?

基本型のみを取得しているようです。つまり、宣言された型であり、実行時の型はありません。


回答:


120

これは少し遅いですが、今日はまったく同じことをしなければなりませんでした。したがって、私の研究に基づいており、gson-2.0を使用する場合は、registerTypeHierarchyAdapterメソッドを使用するのではなく、より平凡なregisterTypeAdapterを使用する必要があります。そしてもちろん、instanceofsを実行したり、派生クラスのアダプターを作成したりする必要はありません。もちろん、派生クラスのデフォルトのシリアル化に問題がなければ、基本クラスまたはインターフェイス用のアダプターは1つだけです。とにかく、これがコードです(パッケージとインポートは削除されました)(githubでも利用可能):

基本クラス(私の場合はインターフェース):

public interface IAnimal { public String sound(); }

2つの派生クラス、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シリアライザー/デシリアライザークラスを実装する必要があるように見えました。


5
Class.forNameを使用したクラス名のシリアル化と(ユーザー入力からの)逆シリアル化は、状況によってはセキュリティに影響を与える可能性があるため、Gson開発チームは推奨していません。code.google.com/p/google-gson/issues/detail?id=340#c2
プログラマブルース

4
シリアライゼーションで無限ループが発生しないようにするにはどうしたらいいですか。context.serialize(src);を呼び出しています。これにより、アダプタが再度呼び出されます。これは私の似たコードで起こったことです。
che javara 2013年

6
違う。このソリューションは機能しません。何らかの方法でcontext.serializeを呼び出すと、無限の再帰が発生します。なぜ実際にコードをテストせずに投稿するのか。2.2.1で試しました。stackoverflow.com/questions/13244769/…で
che javara '28

4
@MarcusJuniusBrutus私はあなたのコードを実行しましたが、スーパーインターフェースIAnimalを定義し、IAnimalAdapterがそれを使用しているため、この特殊なケースでのみ機能するようです。代わりに「猫」しかいなかった場合は、無限再帰の問題が発生します。したがって、このソリューションは一般的なケースではまだ機能しません-共通のインターフェースを定義できる場合のみです。私の場合、インターフェースがなかったので、TypeAdapterFactoryで別のアプローチを使用する必要がありました。
che javara 2014年

2
ユーザーsrc.getClass()。getCanonicalName()の代わりにsrc.getClass()。getName()。これは、コードが内部/ネストされたクラスでも機能することを意味します。
mR_fr0g 2015年

13

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

APIを変更できる場合、Jacksonには現在、比較的単純なポリモーフィックな逆シリアル化のメカニズムがあることに注意してください。いくつかの例をprogrammerbruce.blogspot.com/2011/05/…
プログラマーBruce

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
彼のソリューションを実装した後、私の次の考えはまさにこれを行うことでした:-)
David Levy 14

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

アダプターのSerializeメソッドは、シリアル化するタイプのカスケード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;

逆シリアル化は少しハッキーです。単体テストの例では、Tell-Tale属性の存在をチェックして、デシリアライズするクラスを決定します。シリアル化するオブジェクトのソースを変更できる場合は、インスタンスクラスの名前のFQNを保持する各インスタンスに 'classType'属性を追加できます。ただし、これは非常に非オブジェクト指向です。


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

ここでは、動物、犬、猫のモデルを使用した完全な動作例を掲載しています。

最初から再実装するよりも、このアダプタに依存する方が良いと思います。


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ではなくDog)をに渡すためcontext.deserializeです。これは、使用registerTypeAdapterしない限り、タイプアダプタを呼び出しません。registerTypeHierarchyAdapter


2

更新された回答-他のすべての回答の最良の部分

私はさまざまなユースケースの解決策を説明しており、無限再帰問題にも対処します

  • ケース1:クラスを制御できます。つまり、独自のCatDogおよび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;
    }
}

では、なぜコピーコンストラクタなのでしょうか。

まあ、コピーコンストラクターを定義すると、基本クラスがどれだけ変化しても、ラッパーは同じ役割を継続します。第2に、コピーコンストラクタを定義せずに基本クラスを単にサブクラス化する場合は、拡張クラスの観点から「対話」する必要がありCatWrapperます。コンポーネントが、ラッパー型ではなく、基本クラスの観点から話す可能性は十分にあります。

簡単な代替案はありますか?

確かに、それは今グーグルによって導入されました-これは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代わりにCatDogWrapper代わりにDogAlternativeAnimalAdapter代わりに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

Marcus Junius Brutusの回答とuser2242263の編集を組み合わせると、インターフェースタイプで動作するようにアダプターを定義することで、アダプターに大きなクラス階層を指定する必要がなくなります。次に、インターフェースでtoJSON()およびfromJSON()のデフォルト実装を提供し(これらの2つのメソッドのみが含まれます)、シリアル化する必要があるすべてのクラスにインターフェースを実装させることができます。キャストを処理するために、サブクラスで、インターフェイスタイプから適切なキャストを逆シリアル化して実行する静的なfromJSON()メソッドを提供できます。これは私にとってはうまくいきました(ハッシュマップを含むクラスのシリアライズ/デシリアライズに注意してください-gsonビルダーをインスタンス化するときにこれを追加してください:

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

これが誰かが時間と労力を節約するのに役立つことを願っています!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.