いきさつ
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パターンあります。
- 単一項目(multiple指定なし)
- 配列で要素数が1つ
- 配列で要素数が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つです。
- デシリアライズ時は
isJsonArray()
で配列かどうかチェックして処理を切り替える -
MultipleType
の型パラメータを取得する - シリアライズ時は
isMultiple()
で配列かどうかチェックし処理を切り替える
特に2がちょっと複雑です。
まず、deserialize
メソッドの引数Type typeOfT
が何を表しているかを知る必要があります。
これは、JsonDeserializer
インターフェイスの型パラメータを表すオブジェクトが入っています。
(正確には実行時に実際に使われているオブジェクトの型)
ジェネリクス型の型パラメータはコンパイル時に失われてしまうので、実行時に参照できるようになっているのですね。
定義からわかるのはtypeOfT
はMultipleType<?>
であるということですが、<?>
の部分を知る必要があるため、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の総称型は文章では「ジェネリクス」で、発音は「ジェネリック」としています。
「ジェネリクス」って言いにくくないですか?