Java
Redmine
JSON
GSON

GSONで単一か配列かわからない項目を扱う方法

いきさつ

Redmine REST APIでJSONを取得した際、"custom_fields"に以下のような表記が現れました。

"custom_fields": [
  {
    "id": 1,
    "name": "項目1",
    "value": "a"
  },
  {
    "id": 2,
    "name": "項目2",
    "multiple": true,
    "value": [
      "b"
    ]
  },
  {
    "id": 3,
    "name": "項目3",
    "multiple": true,
    "value": [
      "c",
      "d"
    ]
  },

"value"の内容が"multiple" : trueの場合は配列オブジェクトで、それ以外の場合は単一となるようです。
上記の例の通り3パターンあります。

  1. 単一項目(multiple指定なし)
  2. 配列で要素数が1つ
  3. 配列で要素数が2つ以上

デシリアライズ(JSON→Java)時に単一の場合も全て配列に変換する方法もありますが、シリアライズ時に元の表現に戻せない(単一なのか要素数1の配列かわからない)ので、その制御を入れることを前提として、ついでに汎用的に使える実装を考えてみました。

実装

データオブジェクト

デシリアライズ時にデータを入れるオブジェクトは以下のようになっています。

public class MultipleType<T> {

    private T value;

    private List<T> values;

    public boolean isMultiple() {
        return values != null;
    }

    //以下getter/setter

}

valueまたはvaluesのどちらかを使用し、もう片方はnullとするようにします。

上記の"custom_fields"だと以下のような感じです。

public class CustomField {
    private String id;
    private String name;
    private boolean multiple;
    private MultipleType<String> value;

    //以下getter/setter
}

GSONの機能を極力使用するようにしたいので、JSONとフィールド名は同じにします。

シリアライザ/デシリアライザ

GSONのカスタムシリアライザ/デシリアライザを以下のように実装します。

public class MultipleTypeAdapter implements JsonDeserializer<MultipleType<?>>, JsonSerializer<MultipleType<?>> {

    @Override
    public MultipleType<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException {

        final MultipleType<?> result = new MultipleType<>();

        if (json.isJsonArray()) {
            result.setValues(deserializeArray(json, typeOfT, context));
        } else {
            result.setValue(context.deserialize(json, getGenericType(typeOfT)));
        }
        return result;
    }

    private <T> List<T> deserializeArray(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
        final List<T> values = new ArrayList<>();
        final Type t = getGenericType(typeOfT);
        for (JsonElement e : json.getAsJsonArray()) {
            values.add(context.deserialize(e, t));
        }
        return values;
    }

    /* get actual Type of <?> */
    private Type getGenericType(Type typeOfT) {
        return ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
    }

    @Override
    public JsonElement serialize(MultipleType<?> src, Type typeOfSrc, JsonSerializationContext context) {
        return context.serialize(src.isMultiple() ? src.getValues() : src.getValue());
    }

}

要点は3つです。

  1. デシリアライズ時はisJsonArray()で配列かどうかチェックして処理を切り替える
  2. MultipleTypeの型パラメータを取得する
  3. シリアライズ時はisMultiple()で配列かどうかチェックし処理を切り替える

特に2がちょっと複雑です。
まず、deserializeメソッドの引数Type typeOfTが何を表しているかを知る必要があります。
これは、JsonDeserializerインターフェイスの型パラメータを表すオブジェクトが入っています。
(正確には実行時に実際に使われているオブジェクトの型)
ジェネリクス型の型パラメータはコンパイル時に失われてしまうので、実行時に参照できるようになっているのですね。

定義からわかるのはtypeOfTMultipleType<?>であるということですが、<?>の部分を知る必要があるため、getGenericTypeメソッドでさらに内部の型パラメータを取得しています。

なぜ型パラメータ<?>の実行時型が必要かというと、context.deserializeメソッドで正しい型を与えてあげると、それ以降のデシリアライズはGSONの機能に任せることができるためです。
特にデータオブジェクトのフィールドがプリミティブ型(String含む)ではない型であるような複雑な構造のデシリアライズを手書きせずにすみます。

GSON使用箇所

GSONの使用箇所は以下のようにします。

final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(MultipleType.class, new MultipleTypeAdapter());
final Gson gson = gsonBuilder.create();

MultipleTypeAdapterクラスがジェネリクス型じゃないので警告がでないのがミソだったりします。
カスタムデシリアライザ/シリアライザの型パラメータに<?>(ワイルドカード)を使った理由がこれです。
(なぜワイルドカードならOKなのかの説明はできません・・・)

ところで

私はJavaの総称型は文章では「ジェネリクス」で、発音は「ジェネリック」としています。
「ジェネリクス」って言いにくくないですか?