GSONのJsonObjectでJsonを構築する時はビルダーを作っておくと美しいコードになる


# はじめに

GSONでJavaオブジェクトからJSONを構築するには大雑把に言うと以下の２通りがある。

1. gson.toJson()でオブジェクトを自動でJSONに変換する
1. JsonObjectやJsonWriterを使って自分でJSONを構築する

一般的には1の方法を使用する事が多いけど、2の方法を使わなくてはならないケースもある。

今回は2のJsonObjectを使ってJsonを構築する処理を、直接JsonObjectを使ったコードと、ビルダーを作成して使ったコードを比較してみた。

# サンプルデータ

使用するデータはQiitaのデータ構造を参考にItem、User、Tagの３クラスを用意した。
コンストラクタはlombokを使って生成する。

```java:Item.java
@AllArgsConstructor
public class Item {
    public String id;
    public String title;
    public User user;
    public List<Tag> tags;
}
```
```java:User.java
@AllArgsConstructor
public class User {
    public String id;
    public String name;
}
```
```java:Tag.java
@AllArgsConstructor
public class Tag {
    public String url;
}
```
```java:サンプルデータ
        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")))
        );

```
```json:期待されるJSONの結果
[
  {
    "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が出力されるのかを読み取るのは難しい。

```java:Java7でJsonObjectとJsonArrayを直接使ったコード
        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を使ったコード。
どうしてこうなったってくらい余計にひどくなってしまった。

```java:Java8でJsonObjectとJsonArrayを直接使ったコード
        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](http://docs.oracle.com/javaee/7/api/javax/json/JsonObjectBuilder.html)
[JsonArrayBuilder](http://docs.oracle.com/javaee/7/api/javax/json/JsonArrayBuilder.html)
[Json](http://docs.oracle.com/javaee/7/api/javax/json/Json.html)

```java:JsonObjectBuilder.java
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;
    }
}
```
```java:JsonArrayBuilder.java
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;
    }    
}
```
```java:Json.java
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の構造とほぼ一致するので読み易くなった。

```java:Java8でビルダーを使ったコード
        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文で書き直せば良い。

```java:ToJson.java
public interface ToJson<T> {
    public JsonElement apply(T src);
}
```
```Json.java
    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();
    }
```

ただし呼び出す部分はラムダ式が使えないのでネストが深くなってしまう。
これは元のほうがいいかもしれない。

```java:Java7
        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](https://code.google.com/p/google-gson/)
[Java API for JSON Processing(JSR 353)を触ってみましたー - Challenge Java EE !](http://kikutaro777.hatenablog.com/entry/2013/07/18/193956)