gsonwrapper、その2

2019年5月17日

gsonwrapperについての全投稿は/tag/gsonwrapperにあるので参照されたい。

gsonwrapper~gsonによるJava/JSONシリアライゼーションの続きである。

前回説明した、単純な例の場合の制限事項について、これを回避する方法を説明する。ただし、「単一のオブジェクトが使い回されていた場合でも、JSON表現上では独立したものになってしまう。」を回避することはできない。これはJSONを使う限りは回避不可能だ。

まず、トップレベルがジェネリッククラスの場合を説明する。

トップレベルがジェネリッククラスの場合のシリアライゼーション

前回の例で、シリアライゼーションを行ったFooクラスは、以下の定義になっていた。

  public static class Foo {
    List<Bar>list = new ArrayList<>();
    ...
  }

list変数は、ジェネリッククラスList<Bar>であるのだが、この例の場合には特別な操作をする必要はない。単純にFoo.classを指定してSerializerを作成し、これでシリアライズすればよい。トップレベルがジェネリッククラスでない場合には何もする必要はない。

    Serializer<Foo>serializer = new Serializer<>(Foo.class);
    Foo foo = new Foo();
    serializer.serialize(foo);

しかし、以下のようなケースの場合にはうまくいかない。

  Serializer<List<String>>serializer = new Serializer<>(List<String>.class);
  List<String>list = new ArrayList<>();
  serializer.serialize(list);

まずJava構文としてエラーが発生してしまう。また、クラス中の変数としてジェネリッククラスのある場合と異なり、Javaはこのようなケースでのジェネリック型引数を保持しておくことができない。

このようなケースでは、クラスを指定するのではなく、Gsonに備わるTypeTokenを作成し、それを使用する必要がある。以下のようにする。

import java.util.*;

import org.junit.*;
import static org.junit.Assert.*;

import com.cm55.gson.*;
import com.google.gson.reflect.*;

public class Sample3 {

  @Test
  public void test() {
    Serializer<List<String>> serializer = new Serializer<>(new TypeToken<List<String>>() {});
    String json;
    {
      List<String> list = new ArrayList<String>();
      list.add("A");
      list.add("B");
      json = serializer.serialize(list);
      assertEquals("[\"A\",\"B\"]", json);
    }

    {
      List<String>list = serializer.deserialize(json);
      assertArrayEquals(new String[] { "A", "B" }, list.toArray(new String[0]));
    }
  }
}

TypeTokenは抽象クラスであり、その作成は実際には匿名クラスの作成となる。新たなクラスを作成することにより、ジェネリッククラスの情報がクラスに保持されることになる。

new TypeToken<List<String>>() {}

サブクラスの扱い

もう一点、単純な場合では扱えないケースがあった。それは、プログラムの字面上に現れるクラスのサブクラスのインスタンスが、実際は格納されてしまうケースである。前回の例では、

  public static class Foo {
    List<Bar>list = new ArrayList<>();
    ...
  }

  public static class Bar {
   ...
  }

などとなっていたが、実際には以下かもしれない。

  public static class Foo {
    List<Bar>list = new ArrayList<>();
    ...
  }

  public static class Bar {
   ...
  }
  public static class Bar1 extends Bar {
   ...
  }
  public static class Bar2 extends Bar {
   ...
  }

前回示した単純なやり方では、たとえ実際にはBar1やBar2が使われていても、それらを区別することはできない。特に、デシリアライズ時にはBarとして扱われてしまう。JSON上でこれらを区別する方法が無いからである。再度、前回の例で生成されるJSON文字列を見てみると、以下である。

{"list":[{"a":1,"b":2},{"a":3,"b":4}],"str1":"testString1"}

ここにはシリアライズされた元のクラスが何であったかという情報は無い、フィールド名とその値のみである。したがって、デシリアライズを正しく行うためには、少なくともJSON文字列の中に「どのクラスだったのか」という情報を埋め込んでおく必要がある。

これを行う例としては、以下である。

import static org.junit.Assert.*;

import java.util.*;
import java.util.stream.*;

import org.junit.*;

import com.cm55.gson.*;

public class Sample2 {

  @Test
  public void test() {

    // Fooに対するシリアライザを作成する
    Serializer<Foo>serializer = new Serializer<>(fooHandler);

    // JSON文字列
    String json;

    // オブジェクトを作成し、JSON文字列に変換
    {
      Foo foo = new Foo();
      foo.list.add(new Bar1());
      foo.list.add(new Bar2());
      json = serializer.serialize(foo);
      assertEquals("{\"list\":[{\"T\":\"Bar1\",\"D\":{\"b\":2,\"a\":1}},{\"T\":\"Bar2\",\"D\":{\"c\":3,\"a\":1}}]}", json);
    }

    // JSON文字列からオブジェクトを再構築
    {
      Foo foo = serializer.deserialize(json);
      assertEquals("Bar1 1,2,Bar2 1,3", foo.toString());
    }
  }

  static Handler<Bar>barHandler = new MultiHandlerBuilder<>(Bar.class).addSubClasses(Bar1.class, Bar2.class).build();
  static Handler<Foo>fooHandler = new HandlerBuilder<>(Foo.class).addSubHandler(barHandler).build();

  public static class Foo {
    List<Bar>list = new ArrayList<>();
    @Override
    public String toString() {
      return list.stream().map(b->b.toString()).collect(Collectors.joining(","));
    }
  }

  public static abstract class Bar {
    int a = 1;
  }
  public static class Bar1 extends Bar {    
    int b = 2;
    @Override
    public String toString() {
      return "Bar1 " + a + "," + b;
    }
  }
  public static class Bar2 extends Bar {    
    int c = 3;
    @Override
    public String toString() {
      return "Bar2 " + a + "," + c;
    }
  }
}

上に示したように、Serializer作成時には、クラスやTypeTokenではなく、Handlerというものを指定する必要がある。このHandlerの中に、「いかなるサブクラスが存在しうるのか、その名前を何にしておくのか」を指定しておく必要がある。

つまり、「字面上のクラス」のサブクラスが使用される可能性のある場合には、あらかじめそれを登録しておく必要がある。この詳細としては後述する。