メソッド内に定義されたロカールクラスで Gson().fromJson できない原因を調査してみた

こんな感じのコードでLoader1 クラスを使ってJSONの文字列を読むことができるが Loader2 クラスを使っては読むことができない。

class Test {
    data class Loader1 (val type: String)

    fun test() {
        val jsonString = """{"type": "abc"}"""

        val ret1 = Gson().fromJson(jsonString, Loader1::class.java)
        assert(ret1.type == "abc")

        data class Loader2 (val type: String)
        val ret2 = Gson().fromJson(jsonString, Loader2::class.java)
        assert(ret2.type == "abc") // ret2 は null
    }
}

デコンパイルして Java にしてみる

Show Kotlin bytecode を押す f:id:daisukefuji:20190717143117p:plain

Decompile を押す f:id:daisukefuji:20190717143120p:plain

JavaDecompile されたコードが表示される。 f:id:daisukefuji:20190717143124p:plain

以下抜粋した Java のコード

public final class Test {
   public final void test() {
         final class Loader2 {
            @NotNull
            private final String type;

            @NotNull
            public final String getType() {
               return this.type;
            }

            public Loader2(@NotNull String type) {
               this.type = type;
            }
         }

         Loader2 ret2 = (Loader2)(new Gson()).fromJson(jsonString, Loader2.class);
      }
   }

Java の local class が使われていることが確認できた。

続いて Gson が local class をどのように扱うのか調べる。

skipDeserializetrue なので null を返す。

      @Override public T read(JsonReader in) throws IOException {
        if (skipDeserialize) {
          in.skipValue();
          return null;
        }
        return delegate().read(in);
      }

https://github.com/google/gson/blob/9d44cbc19a73b45971c4ecb33c8d34d673afa210/gson/src/main/java/com/google/gson/internal/Excluder.java#L125

skipDeserialize 以下で excludeClasstrue になっている。

    boolean excludeClass = excludeClassChecks(rawType);

    final boolean skipSerialize = excludeClass || excludeClassInStrategy(rawType, true);
    final boolean skipDeserialize = excludeClass ||  excludeClassInStrategy(rawType, false);

https://github.com/google/gson/blob/9d44cbc19a73b45971c4ecb33c8d34d673afa210/gson/src/main/java/com/google/gson/internal/Excluder.java#L113

excludeClass に以下のコードでtrue が代入されていた。

      if (isAnonymousOrLocal(clazz)) {
          return true;
      }

isAnonymousOrLocal の実装は以下のようになっておりLoader2.class.isLocalClass() == true なので fromJson できなかった。

  private boolean isAnonymousOrLocal(Class<?> clazz) {
    return !Enum.class.isAssignableFrom(clazz)
        && (clazz.isAnonymousClass() || clazz.isLocalClass());
  }

なぜGsonはAnonymousかLocalクラスを使ってロードできないのか調べていたら以下のissueにたどり着いた。

github.com

Don't use double brace initialization. It prevents serialization and Gson is designed for symmetric serialization and serialization. Original comment by limpbizkit on 7 Feb 2013 at 1:02 Changed state: WontFix

で、最終的にちゃんとユーザーガイドに書いてあることに気づいた。

Gson can also deserialize static nested classes. However, Gson can not automatically deserialize the pure inner classes since their no-args constructor also need a reference to the containing Object which is not available at the time of deserialization. You can address this problem by either making the inner class static or by providing a custom InstanceCreator for it. Here is an example:

sites.google.com

結論: ちゃんとドキュメントを読みましょう。