はじめに
GSONでJavaオブジェクトからJSONを構築するには大雑把に言うと以下の2通りがある。
- gson.toJson()でオブジェクトを自動でJSONに変換する
- JsonObjectやJsonWriterを使って自分でJSONを構築する
一般的には1の方法を使用する事が多いけど、2の方法を使わなくてはならないケースもある。
今回は2のJsonObjectを使ってJsonを構築する処理を、直接JsonObjectを使ったコードと、ビルダーを作成して使ったコードを比較してみた。
サンプルデータ
使用するデータはQiitaのデータ構造を参考にItem、User、Tagの3クラスを用意した。
コンストラクタはlombokを使って生成する。
@AllArgsConstructor
public class Item {
public String id;
public String title;
public User user;
public List<Tag> tags;
}
@AllArgsConstructor
public class User {
public String id;
public String name;
}
@AllArgsConstructor
public class Tag {
public String url;
}
List<Item> items = Arrays.asList(
new Item("item1", "title1",
new User("user1", "user1"),
Arrays.asList(new Tag("tag1"), new Tag("tag2"))),
new Item("item2", "title2",
new User("user2", "user2"),
Arrays.asList(new Tag("tag2"), new Tag("tag3")))
);
[
{
"id": "item1",
"title": "title1",
"user": {
"id": "user1",
"name": "user1"
},
"tags": [
{"url": "tag1"},
{"url": "tag2"}
]
},
{
"id": "item2",
"title": "title2",
"user": {
"id": "user2",
"name": "user2"
},
"tags": [
{"url": "tag2"},
{"url": "tag3"}
]
}
]
JsonObjectを直接使ったコード
まずはJsonObjectとJsonArrayを直接使ったJava7のコード。
複雑なコードではないけど、ここからどういうJSONが出力されるのかを読み取るのは難しい。
JsonArray array = new JsonArray();
for (Item item : items) {
JsonObject itemJson = new JsonObject();
itemJson.addProperty("id", item.id);
itemJson.addProperty("title", item.title);
JsonObject userJson = new JsonObject();
userJson.addProperty("id", item.user.id);
userJson.addProperty("name", item.user.name);
itemJson.add("user", userJson);
JsonArray tagArray = new JsonArray();
for (Tag tag : item.tags) {
JsonObject tagJson = new JsonObject();
tagJson.addProperty("url", tag.url);
tagArray.add(tagJson);
}
itemJson.add("tags", tagArray);
array.add(itemJson);
}
次にJava8のStreamAPIを使ったコード。
どうしてこうなったってくらい余計にひどくなってしまった。
Collector<JsonElement, JsonArray, JsonArray> toJsonArray = Collector.of(
JsonArray::new,
JsonArray::add,
(a, b) -> { a.addAll(b); return a; }
);
JsonArray array = items.stream()
.map(item -> {
JsonObject itemJson = new JsonObject();
itemJson.addProperty("id", item.id);
itemJson.addProperty("title", item.title);
JsonObject userJson = new JsonObject();
userJson.addProperty("id", item.user.id);
userJson.addProperty("name", item.user.name);
itemJson.add("user", userJson);
JsonArray tagArray = item.tags.stream()
.map(tag -> {
JsonObject tagJson = new JsonObject();
tagJson.addProperty("url", tag.url);
return tagJson;
})
.collect(toJsonArray);
itemJson.add("tags", tagArray);
return itemJson;
})
.collect(toJsonArray);
return array;
ビルダーを作って改善
先のコードを改善するためにJSONを構築する為のビルダーを作ってみる。
参考にしたのは Java API for JSON Processing(JSR 353) のビルダークラス。
これにStreamAPI用のメソッドをいくつか追加した。
JsonObjectBuilder
JsonArrayBuilder
Json
public class JsonObjectBuilder {
private JsonObject jsonObject = new JsonObject();
public JsonObject build() {
return jsonObject;
}
public JsonObjectBuilder add(String name, JsonElement json) {
jsonObject.add(name, json);
return this;
}
public JsonObjectBuilder add(String name, String value) {
jsonObject.addProperty(name, value);
return this;
}
}
public class JsonArrayBuilder {
private JsonArray jsonArray = new JsonArray();
public JsonArray build() {
return jsonArray;
}
public JsonArrayBuilder add(JsonElement json) {
jsonArray.add(json);
return this;
}
public JsonArrayBuilder addAll(JsonArrayBuilder builder) {
jsonArray.addAll(builder.build());
return this;
}
public JsonArrayBuilder add(String value) {
jsonArray.add(new JsonPrimitive(value));
return this;
}
}
public class Json {
private Json() {}
public static JsonObjectBuilder createObjectBuilder() {
return new JsonObjectBuilder();
}
public static JsonArrayBuilder createArrayBuilder() {
return new JsonArrayBuilder();
}
public static <T> JsonArray toJsonArray(Collection<T> list, Function<T, JsonElement> toJson) {
return list.stream()
.map(toJson)
.collect(toJsonArray());
}
public static Collector<JsonElement, JsonArrayBuilder, JsonArray> toJsonArray() {
return Collector.of(
Json::createArrayBuilder,
JsonArrayBuilder::add,
JsonArrayBuilder::addAll,
JsonArrayBuilder::build);
}
}
はじめに書いたコードをビルダーを使って書き直してみる。
コードの構造がJsonの構造とほぼ一致するので読み易くなった。
JsonArray itemArray =
Json.toJsonArray(items, item -> Json.createObjectBuilder()
.add("id", item.id)
.add("title", item.title)
.add("user", Json.createObjectBuilder()
.add("id", item.user.id)
.add("name", item.user.name)
.build()
)
.add("tags", Json.toJsonArray(item.tags, tag -> Json.createObjectBuilder()
.add("url", tag.url)
.build()
))
.build()
);
Java7でも使えるようにする
Java7でも使えるようにするには、Functionに相当するインターフェイスを作り、StreamAPIを使う部分をfor文で書き直せば良い。
public interface ToJson<T> {
public JsonElement apply(T src);
}
public static <T> JsonArray toJsonArray2(Collection<T> list, ToJson<T> toJson) {
JsonArrayBuilder array = Json.createArrayBuilder();
for (T e : list) {
array.add(toJson.apply(e));
}
return array.build();
}
ただし呼び出す部分はラムダ式が使えないのでネストが深くなってしまう。
これは元のほうがいいかもしれない。
JsonArray itemArray =
Json.toJsonArray2(items, new ToJson<Item>() {
@Override
public JsonElement apply(Item item) {
return Json.createObjectBuilder()
.add("id", item.id)
.add("title", item.title)
.add("user", Json.createObjectBuilder()
.add("id", item.user.id)
.add("name", item.user.name)
.build()
)
.add("tags", Json.toJsonArray2(item.tags, new ToJson<Tag>() {
@Override
public JsonElement apply(Tag tag) {
return Json.createObjectBuilder()
.add("url", tag.url)
.build();
}
}))
.build();
}
});
最後に
GSONに限らず、良く出来たライブラリであっても部分的に使いにくい箇所やプロジェクトでの使用形態と合わないという事はよくある。
「このライブラリは使えない」と判断する前にユーティリティクラス等での改善を検討する事も大事だと思った。
今回はGSONのJsonObjectを使った例を挙げたけど、JsonWriterを使った場合やGson以外のJsonライブラリでも応用できるんじゃないかな。
参考
google-gson
Java API for JSON Processing(JSR 353)を触ってみましたー - Challenge Java EE !