Retrofitを使ってややこしいJSONをモデル（POJO）にパースする

※初投稿は2015年05月20日 14時21分 (JST)です。
2017年現在では通用しない可能性があります。
ご了承ください。




WebAPIからJSONを取得してモデル（POJO）にパースするというよくある処理を実装しようとしてRetrofitを使っていたのですが
APIが返すJSONが一曲あってそのままでは使えなかったのでGSONにパースするための方法を調べました。

## すんなりパースできないJSON

1. JSONに不要なメタデータが付帯している
1. JSONに階層がある
1. キーが不定なフィールドがある


```json:item.json
{
  "items": [
    {
      "id": "0001",
      "target": {
        "name":"車",
        "color":"red",
        "prices":{
          "default":1000
        }
      },
      "updated": "2015/01/01"
    },
    {
      "id": "0002",
      "target": {
        "name":"自転車",
        "color":"blue",
        "prices":{
          "default":1000,
          "sale":800  //データが可変
        }
      },
      "updated": "2015/01/02"
    }
  ],
  "meta": "hoge",
  "data": "huga",
  "timestamp": "2015/05/08 17:26:55"
}
```

1. ほしいのはitemsの持っている商品情報のみ

2. itemsの中にも更に階層がある。（今回はtargetがItemクラスに相当）
ちょっと特殊なケースな気もします。

3. prices（商品の価格）
　　通常時、割引時、など場合によって値が増減する場合


##パースしたいモデル

JSON→POJOのコード生成にはjsonschema2pojoを使いました。(http://www.jsonschema2pojo.org/)



```java:model.java

public class Item {

    @Expose
    private String name;

    @Expose
    private String color;

    @Expose
    private HashMap<String, Long> prices;

	// Serialize/Deserialize不要のフィールドは@Exposeアノテーションを付けない
	private String notExpose; 

//	getter/setter...

}
```
3.jsonschema2pojoを使うとPricesクラスを作ってしまうので困ってたのですが、単に型をマップにしてあげるだけでした。



##TypeAdapterをカスタマイズする。
シリアライズしたいノードはtargetなので、
対象のノードまで辿ってパースできるようにTypeAdapterFactoryを拡張します。


```java:ItemAdapterFactory.java
public class ItemAdapterFactory implements TypeAdapterFactory{

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        final TypeAdapter<T> delegate          = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> adapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            @Override
            public T read(JsonReader in) throws IOException {
                // in → JsonTreeReader:JSONを再帰的にリーフまで読みこむ。

                JsonElement element = getTargetElement(adapter.read(in));

                return delegate.fromJsonTree(element);
            }
        };
    }

	/* 対象ノードを探す */
    private JsonElement getTargetElement(JsonElement element) {

		if(!element.isJsonObject()) return element;
		 
		JsonObject obj = element.getAsJsonObject();

		/* targetにたどり着くまで必要なnodeを教えてあげる */
       if (obj.has("items")) {
			return obj.get("items");
       } else if (obj.has("target")) {
			return obj.get("target");
       }
    }
}
```

呼び出し側

```java:
  public Observable<List<Item>> getItemObs(){

        Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapterFactory(new ItemAdapterFactory())
                .create();

        RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint("http://hogehoge")
                .setConverter(new GsonConverter(gson))
                .build();

        ItemService service = restAdapter.create(ItemService.class);

        return  service.obsItemList();
    }
```


##参考URL
[Retrofitでニコニコ動画APIのレスポンスをそのままではパースできない - Qiita](http://qiita.com/yamagen0915/items/b0396cacde000cd53acd)

##備考

JSON ←→ POJO ←→ SQLite/ActiveAndroid/Realm
この辺ってどれか一個あればあとは同じ形なので
一気通貫にコード生成してくれるツールってないんですかね？