KotlinでtoJsonとfromJsonのJSONパース。MoshiのCustom Type Adaptersを使ってオブジェクトのテストを快適に。

最近はkotlinで開発しています。これまで他の言語で出来ていたこともkotlinではどうやって出来るのか?、調査したり試したりすることは楽しいですね。新しい言語に触れる醍醐味とも言えます。
kotlinをサーバサイドのメイン言語に使っています。数ある処理の中でもJSONの扱いは必須です。
今のプロジェクトではMoshiライブラリをメインで使っています。今回はkotlinでMoshiライブラリを使った話です。

Moshi

MoshiはJavaで開発されたJSONライブラリです。

github.com

ライブラリの特徴は公式のほうを参照いただくとして、メインの話をしたいのが「Custom Type Adapters」です。
Moshiは自作のアダプタクラスに@ToJson と @FromJsonのアノテーションがついたメソッドを実装すればプリミティブな型以外でも追加した型をオブジェクトからjson文字列へ(toJson)、json文字列からオブジェクトへ(fromJson)の変換処理を自作することができます。

テストを書くときにCustom Type Adaptersを活用する

今のプロジェクトではelasticsearchを使っていて検索パラメータを定義するビルダークラスの整合性の検証にMoshiのCustom Type Adaptersを活用しました。
カスタムアダプタを使いelasticsearchのAggregationBuildersクラスをkotlinのdata classへ変換し検索パラメータの検証を行いました。

ビルダークラスはgetAggregationsQuery()をコールすることで取得できます。

object AggregationModel {
    fun getAggregationsQuery() = AggregationBuilders.terms("aggs_post_id").field("post_id").size(100)
                .subAggregation(AggregationBuilders.terms("aggs_category_id").field("category_id").size(200)
                        .subAggregation(AggregationBuilders.terms("aggs_user_id").field("user_id").size(300)))!!
}

このビルダークラスをjson文字列で表すと次のような構造になります。

{
    "aggs_post_id": {  "terms": { "field": "post_id", "size": 100 }, "aggregations": {
            "aggs_category_id": { "terms": { "field": "category_id", "size": 200 }, "aggregations": {
                    "aggs_user_id": { "terms": { "field": "user_id", "size": 300 } } } } }
}
  • ネストが深い(key1.aggregations.key2.aggregations.key3.aggregations.... と幾重にも定義できる)
  • keyの文字列はこちらで決めれる( aggs_post_id, aggs_category_id、aggs_user_id)
  • key.aggregationsの指定は任意

kotlinのdata classで表すと次のように定義できます。

data class Aggregations(
        val name: String,
        val terms: Terms,
        val aggregations: Aggregations?
)

data class Terms(
        val field: String,
        val size: Long
)

今回のやりこと

今回、やりたいことを明確にします。

AggregationBuildersクラスをjson文字列変換し、MoshiのCustom Type Adaptersを使い、kotlinのdata classに変換してテストを快適にしたい。

です。

AggregationBuildersクラスはjson文字列に変換できる

AggregationBuildersはXContentFactoryでパラメータ構造を文字列に変換できます。

val aggregations = AggregationModel.getAggregationsQuery()

val builder = XContentFactory.jsonBuilder()
builder.startObject()
aggregations.toXContent(builder, org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS)
builder.endObject()

val json = builder.string()

val json = builder.string()ではビルドしたパラメータがjson文字列として格納されています。こちらのjson文字列を実装したカスタムアダプタでkotlinのdata classに変換します。

AggregationAdapterクラスを実装する

次のように定義することでMoshiがサポートしている型ではないAggregations型を追加できます。

class AggregationAdapter : JsonAdapter<Aggregations>() {

    companion object {
        val FACTORY: Factory = object : Factory {
            override fun create(type: Type, annotations: Set<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
                if (type === Aggregations::class.java) {
                    return AggregationAdapter()
                }
                return null
            }
        }
    }

次のページを参考にさせていただきました。
Retrofit2.0に備えてKotlinで始めるMoshi(JSONパーサ) - Qiita

今回はJsonWriterとJsonReaderを使って@ToJson、@FromJsonを実装しました。

@ToJson
override fun toJson(writer: JsonWriter?, value: Aggregations?)

@FromJson
override fun fromJson(reader: JsonReader?): Aggregations?

keyの値に"aggs_post_id"など独自定義ができたり、ネストが深かったりとJsonWriterとJsonReaderを使うことで複雑な構造であっても変換処理を実装することができます。

実際のコードは省略してしまっていますが公開していますので、よろしければ参照してください。役立つと嬉しいです。
github.com

実装したアダプタクラスを使いdata class化することでアサーションも見通しが良くなりました。

val adapter = Moshi.Builder().add(AggregationAdapter()).build().adapter(Aggregations::class.java)

val actual = adapter.fromJson(json)

actual.name shouldBe "aggs_post_id"
actual.terms.field shouldBe "post_id"
actual.terms.size shouldBe 0L

actual.aggregations?.name shouldBe "aggs_category_id"
actual.aggregations?.terms?.field shouldBe "category_id"
actual.aggregations?.terms?.size shouldBe 0L

actual.aggregations?.aggregations?.name shouldBe "aggs_user_id"
actual.aggregations?.aggregations?.terms?.field shouldBe "user_id"
actual.aggregations?.aggregations?.terms?.size shouldBe 0L

まとめ

開発においてjsonの扱いは必須です。
kotlinでもMoshiのCustom Type Adaptersを使うことで独自の型の変換を学べました。
JsonWriterとJsonReaderのを使い変換処理をコード化することは慣れが必要で扱いにくいと感じてしまうかもしれません。

APIのResponse Bodyからオブジェクトへの変換をする、よくあるパターンです。
今回の学びでkotlinでも臆することなく向き合えるようになりました!