Android
Kotlin
Moshi
JsonAdapter

MoshiのCustomJsonAdapterをもしもしする

「代わりに投稿」で失礼します。
はじめまして @halu5071 と申します。

今更感満載ですが、MoshiのカスタムJsonAdapterを実装するところまでを書いてみました。

忘備録的に書いたので、文章がぶっきらぼうですがご容赦ください。


Moshiを使っていて、デフォルトのMoshiでパース出来ないJsonがあった場合、カスタムJsonAdapterを作る必要がある。
そのまとめ

Moshi: v1.5.0

こんな感じのJsonがあったとする

sample.json
{
  "data": [
    {
      "id": 4,
      "title": "タイトル1",
      "body": "本文1",
      "url": "https://sample.com/data/4",
      "created_at": "2016-04-15T18:19:03+09:00",
      "user": {
        "id": 3,
        "name": "user3",
        "profile_image_url": "https://image.sample.com/uploads/sample.jpg"
      },
      "comments": [
        {
          "id": 7,
          "body": "コメント",
          "created_at": "2016-05-13T17:07:18+09:00",
          "user": {
            "id": 2,
            "name": "user2",
            "profile_image_url": "https://image.sample.com/uploads/sample.jpg"
          }
        }
      ]
    },
    {
        //その他のデータ
    }
  ],
  "meta": {
    "previous_page": null,
    "next_page": "https://api.sample.com/teams/sample/posts?page=2&per_page=20",
    "total": 39
  }
}

Moshiのデフォルトパーサーは、Arrayのパース時にjsonの先頭に[があるかないかで判別しているが、この場合data以下にArrayがあるので正しくパースされない。なので、data以下だけを取り出してArrayとして処理させたい。

調べてみる

JsonReader

まずMoshiのJsonAdapterで実際にJsonを読み取るJsonReaderの仕組みを見てみる。

Token

Jsonの各構成要素を表したenum型。例えば配列の始まりを表す[などは、BEGIN_ARRAYと表現されている。JsonReaderはここを見て、どのような処理をするのか決定する。
例えば、beginArray()の場合Javadocにあるように

Consumes the next token from the JSON stream and asserts that it is the beginning of a new array.
(訳)Json文字列のなかから次のtokenを処理し、新しい配列の最初に挿入します

実際のCustomeJsonAdapterでfromJson()メソッドなどを使う際には、処理したいtokenに対応するメソッドを呼んでやる。メソッドの中にはwithout consumingとJavadocにあるように、処理をせず、あるかないかだけを判別するメソッドもあるのでうまく使ってやる。

JsonAdapter

Moshiでパースする際には、adapter()メソッドで対象となるアダプターを取得し、fromJson()でパースする。
Moshiがアダプターをつくる際には、Moshiに登録されたFactory全てにAdapterを作らせる(コードはここ)。その結果がnullでは無かった場合のみ、対応するAdapterが返却され、fromJsonメソッドでパースが出来る。

なので、adapter(Type type)メソッドで指定するTypeと、カスタムJsonAdapterのFactoryクラスで指定するTypeは一致している必要がある。

例えばこう。

CustomJsonAdapter.kt
class CustomJsonAdapter {
  ...
  companion object {
    val FACTORY: Factory = object : Factory {
      override fun create(type: Type, annotations: MutableSet<out Annotation>?, moshi: Moshi): CustomJsonAdapter? {
        val listType = Types.newParameterizedType(List::class.java, DataEntity::class.java)
        if (type == listType) {
          // ここでCustomJsonAdapterを返却
        }
        return null
      }
    }
  }
}
val moshi = Moshi.Builder()
                 .add(CustomJsonAdapter.FACTORY)
                 .build()
val type: Type = Types.newParameterizedType(List::class.java, DataEntity::class.java)
val jsonAdapter = moshi.adapter<List<DataEntity>>(type)

また、対応していないFactoryであることを示すために、ちゃんとnullを返す必要がある。

実際にカスタムする

やっと戻ってこれた。最初に示したJsonをパースする。

再掲

sample.json
{
  "data": [
    {
      "id": 4,
      "title": "タイトル1",
      "body": "本文1",
      "url": "https://sample.com/data/4",
      "created_at": "2016-04-15T18:19:03+09:00",
      "user": {
        "id": 3,
        "name": "user3",
        "profile_image_url": "https://image.sample.com/uploads/sample.jpg"
      },
      "comments": [
        {
          "id": 7,
          "body": "コメント",
          "created_at": "2016-05-13T17:07:18+09:00",
          "user": {
            "id": 2,
            "name": "user2",
            "profile_image_url": "https://image.sample.com/uploads/sample.jpg"
          }
        }
      ]
    },
    {
        //その他のデータ
    }
  ],
  "meta": {
    "previous_page": null,
    "next_page": "https://api.sample.com/teams/sample/posts?page=2&per_page=20",
    "total": 39
  }
}

これに対応するモデルをDataEntity.ktとしてカスタムJsonAdapterを作る。

Factoryを作る

Moshiのコードを見る限り、他のデフォルトJsonAdapterは内部にFactoryクラスを持っているよう(コード)なので、同じように実装してみる。

CustomJsonAdapter.kt
class CustomJsonAdapter(
  val moshi: Moshi) : JsonAdapter<List<DataEntity?>>() {

  override fun fromJson(reader: JsonReader): List<DataEntity?>? {
    // 後で
  }

  override fun toJson(writer: JsonWriter?, value: List<DataEntity?>?) {
    // 後で
  }

  companion object {
    val FACTORY: Factory = object : Factory {
      override fun create(type: Type, annotations: MutableSet<out Annotation>?, moshi: Moshi): CustomJsonAdapter? {
        val listType = Types.newParameterizedType(List::class.java, DataEntity::class.java)
        if (type == listType) {
          return CustomJsonAdapter(moshi)
        }
        return null
      }
    }
  }
}

このAdapterの引数にmoshiを渡しているのは、DataEntity一つひとつのパースを、デフォルトのAdapterに任せたいため。

fromJson()

パースしたいJsonを見ると、{から始まり、dataという名前があり、[と続きます。

これらはそれぞれ、JsonReaderに対応する処理メソッドがあるので、これを使ってあげて、

CustomJsonAdapter.kt
...
  override fun fromJson(reader: JsonReader): List<DataEntity?>? {
    reader.beginObject()
    if (reader.nextName() != "data") {
      return null
    }
    reader.beginArray()
    //これから追記
    reader.endArray()
  }
...

と大枠が出来上がります。簡単のため、Jsonのmetaの部分はパースしません。

reader.beginArray()のあとに、配列の中の処理が始まるのでここでデフォルトのAdapterに任せます。

CustomJsonAdapter.kt
...
  override fun fromJson(reader: JsonReader): List<DataEntity?>? {
    reader.beginObject()
    if (reader.nextName() != "data") {
      return null
    }
    reader.beginArray()
    val list = arrayListOf<DataEntity?>()
    val adapter = moshi.adapter(DataEntity::class.java)
    while (reader.hasNext()) {
      list.add(adapter.fromJson(reader))
    }
    reader.endArray()

    return list
  }
...

これで完成

※ toJson()は省略します

おわりに

Javadoc読むの大事。コード読むの大事
スレッドごとに違うAdapterを使うために、ThreadLocalなどを使っていたのでもう少し色々調べてみようかと思います。
Factoryで実装する意味も考えます。