【Kotlin1.4】kotlinx.serializationとMoshiを比較してみた


# 背景

先日
[Kotlin1.4で使いたい新機能を一部紹介する](https://qiita.com/sudo5in5k/items/e9207d39fbbfdfe827b2)
という記事を書かせていただいて、その中で`kotlinx.serializaton`が気になっていました。

JSONパーサのライブラリは色々ありますが、[「Gson is deprecated.」](https://www.reddit.com/r/androiddev/comments/ejjroe/kotlinx_json_vs_gson_a_short_article_about_kotlin/fcz3v0d/?utm_source=share&utm_medium=web2x)から[Moshi](https://github.com/square/moshi)を使っている方が多いのではないでしょうか。

ですがこの度、Kotlin1.4になってからStableなJSON serializationとして[kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization)が発表されました。
https://kotlinlang.org/docs/reference/whatsnew14.html#stable-json-serialization

現状は`1.0.0-RC`ですがdocumentも整理され、`1.0.0`になるのも近いと思いますのでこのタイミングで`Moshi`と実装方法を比較してみたいと思います。

※この記事はどちらが優れているかを判断するわけではありません

# 導入

今回はAndroid Studioでの導入を想定しています
他PFで導入方法を知りたい方は[こちら](https://github.com/Kotlin/kotlinx.serialization)を参照ください

## Kotlinを1.4へ

* Android Studioを最新版へ (Previewではない方が安定するとは思います、私は執筆時点では4.0.1を使用しています)
* `Languages & Frameworks` -> `Kotlin`から1.4.0をinstall
![スクリーンショット 2020-08-29 2.47.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260353/84381ccc-e20b-d319-f2e4-0fd5c3676aca.png)

* (おそらく任意)`Kotlin Code Migrations`を行う
1.4.0 install後に、右下に出てきたのでやっています
![スクリーンショット 2020-08-29 2.52.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260353/a8e9e773-30da-7897-d2b3-125caf7f7ac7.png)

※環境によっては出てくるタイミングが違うかもしれません

* Android Studioを再起動する

## Build.gradleの変更

## ./build.gradleの変更

* ext.kotlin_versionを1.4.0へ
* dependenciesに`classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"`を追加

```build.gradle
buildscript {
    ext.kotlin_version = '1.4.0' //変更
(略)

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" //追加
    }
```

## ./app/build.graleの変更

* apply pluginに`kotlinx-serialization`を追加
* dependenciesに`org.jetbrains.kotlinx:kotlinx-serialization-core`を追加

※stableになる前に導入している方は`dependencies`に`kotlinx-serialization-runtime`を記述していると思いますが、それは古いため新しく`core`を使うことになります

```app/build.gradle

apply plugin: 'kotlinx-serialization' // 追加

dependencies {
    def serialization_version = "1.0.0-RC"
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version" // 追加
}

```

これで準備は整いました

# 実装

ここからはMoshiと比較して以下のケースを考えてみます

1. Retrofitを介してデータをとってくる
2. encodeする
3. decodeする

## Retrofitを使ってAPIからのResponseをデータクラスに変換する場合

### 環境

RetrofitとそのAdapterをgradleに追加します

```./app/build.gradle
// Retrofit
def retrofit_ver = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_ver"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_ver"

def serialization_version = "1.0.0-RC"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version"
def serialization_converter_version = "0.6.0"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:$serialization_converter_version"

// Moshi
def moshi_version = "1.9.2"
implementation "com.squareup.moshi:moshi:$moshi_version"
implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
```

さすがJake神、kotlin1.4.0 & serialization 1.0.0-RCに対応しています。仕事が速いです　
https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter/commit/8976bafb376c32a3cc689bc86c782f7714a9d4a7

### 使用するAPI

今回は対象に[Qiita Apiv2からユーザー一覧](https://qiita.com/api/v2/docs#%E3%83%A6%E3%83%BC%E3%82%B6)を取得することを考えてみます

APIは`Retrofit`を使ってこのように定義しました
こちらは`Moshi`でも`kotlinx.serialization`でも変わりはありません

```QiitaUserApi.kt
import com.example.pagingsandbox.data.entity.QiitaUserEntity
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface QiitaUserApi {
    @GET("users")
    suspend fun getUsers(@Query("page") page: Int): Response<List<QiitaUserEntity>>

    companion object {
        const val BASE_URL = "https://qiita.com/api/v2/"
    }
}
```

`QiitaUserEntity`はAPI Responseからデータをとりたいフィールドを定義した`data class`です


### Moshiの場合

#### entityの定義
Data classであるQiitaUserEntityはこのようになります

```QiitaUserEntitiy.kt
import com.squareup.moshi.Json
import kotlinx.serialization.Serializable

@JsonClass(generateAdapter = true)
data class QiitaUserEntity(
    val description: String?,
    @Json(name = "profile_image_url")
    val profileImageUrl: String,
    val id: String,
    @Json(name = "items_count")
    val itemsCount: Int?
)
```

* 実際のAPIフィールド名と関連付ける場合は、`@Json(name="property_name")`という形で関連付け
* `@JsonClass(generateAdapter = true)`でAdapterの考慮をする

#### Retrofitの定義

```QiitaUserService.kt
object QiitaUserService {
    operator fun invoke(): QiitaUserApi {
        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
        val builder = Retrofit.Builder()
            .baseUrl(QiitaUserApi.BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
        return builder.create(QiitaUserApi::class.java)
    }
}
```

MoshiConverterFactoryが必要


### kotlinx.serializationの場合

#### entityの定義

```QiitaUserEntity.kt
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class QiitaUserEntity(
    val description: String?,
    @SerialName("profile_image_url")
    val profileImageUrl: String,
    val id: String,
    @SerialName("items_count")
    val itemsCount: Int?
)
```

* `@Serializable`はseriazalitonをするために必要なもの
* `@SerialName`で実際のAPIフィールド名と関連付け

kotlinxだけでまとまっているのは気持ちがいいですね

#### Retforitの定義

```QiitaUserService.kt
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit

@ExperimentalSerializationApi
object QiitaUserService {
    operator fun invoke(): QiitaUserApi {
        val contentType = "application/json".toMediaType()
        val format = Json { ignoreUnknownKeys = true }
        val builder = Retrofit.Builder()
            .baseUrl(QiitaUserApi.BASE_URL)
            .addConverterFactory(format.asConverterFactory(contentType))
            .build()
        return builder.create(QiitaUserApi::class.java)
    }
}
```

違いとしては、addConverterFactory部分で`format.asConverterFactory`としています。
ここがMoshiとは少し違うところです。

ここでの`format`は`kotlinx.serialization`の`Json instance`を作成しており、その中の`configuration`を変えています。APIからデータクラスに定義していない他のフィールドも渡ってくるからです。

ここでの`ignoreUnknownKeys = true`は今回のデータクラスに定義していないフィールドを無視するということです。
逆にAPIからデータクラス外のフィールドが渡ってきた場合、対応できず以下のようなexceptionが発生します。

```kotlin
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 60: Encountered an unknown key 'language'.
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
```

`JsonBuilder`から拡張したものを使いなさいといっていますね。ちょっと中身を見てみましょう。

##### Json {} について

以下の`Json.kt`は`kotlin-serialization-core`のもので、中で`configuration`をセットしているのがわかるかと思います。

```Json.kt
/**
 * Creates an instance of [Json] configured from the optionally given [Json instance][from] and adjusted with [builderAction].
 */
public fun Json(from: Json = Json.Default, builderAction: JsonBuilder.() -> Unit): Json {
    val builder = JsonBuilder(from.configuration)
    builder.builderAction()
    val conf = builder.build()
    return JsonImpl(conf)
}
```

そして、その`configuration`には何が設定できるかというと、

```JsonConf.kt
package kotlinx.serialization.json.internal

import kotlinx.serialization.*
import kotlinx.serialization.modules.*
import kotlin.jvm.*

// Mirror of the deprecated JsonConfiguration. Not for external use.
@OptIn(ExperimentalSerializationApi::class)
internal data class JsonConf(
    @JvmField public val encodeDefaults: Boolean = true,
    @JvmField public val ignoreUnknownKeys: Boolean = false,
    @JvmField public val isLenient: Boolean = false,
    @JvmField public val allowStructuredMapKeys: Boolean = false,
    @JvmField public val prettyPrint: Boolean = false,
    @JvmField public val prettyPrintIndent: String = "    ",
    @JvmField public val coerceInputValues: Boolean = false,
    @JvmField public val useArrayPolymorphism: Boolean = false,
    @JvmField public val classDiscriminator: String = "type",
    @JvmField public val allowSpecialFloatingPointValues: Boolean = false,
    @JvmField public val serializersModule: SerializersModule = EmptySerializersModule
)
```

このように様々な設定ができます。デフォルトでは`ignoreUnknownKeys = false`なので例外が発生するというわけですね。
全ては紹介しきれないので実際に動かして確かめてみてください！
[公式のmdファイル](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#json-configuration)の`configuration`が参考になると思います。

##### Json (デフォルト)


もし、デフォルトの設定でも構わないという場合は、

```snippet.kt
Json.asConverterFactory
```

という形で記述することができます。このJsonは以下のDefaultを呼び出しています。

```Json.kt
@OptIn(ExperimentalSerializationApi::class)
public sealed class Json(internal val configuration: JsonConf) : StringFormat {

    override val serializersModule: SerializersModule
        get() = configuration.serializersModule

    /**
     * The default instance of [Json] with default configuration.
     */
    public companion object Default : Json(JsonConf())
```

## JsonStringからデータクラスに変換する場合

上述している`QiitaUserEntity`をそのまま使用して、以下のようなjsonStringを定義します。

```snippet.kt
fun getJsonString() = """
        {
            "description": "description",
            "profile_image_url": "https://pbs.twimg.com/profile_images/1201406146822557696/ewFFvnAa_400x400.jpg",
            "id": "001",
            "items_count": null
        }
        """.trimIndent()
```

### Moshiの場合

```kotlin
val adapter = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
            .adapter(QiitaUserEntity::class.java)
adapter.fromJson(getJsonString())
```

### kotlinx.serializationの場合

```kotlin
Json.decodeFromString<QiitaUserEntity?>(getJsonString())
```

`decodeFromString`を呼び出します。
かなりシンプルですね、以前は`Json.parse<T>(string: String)`というものがあったのですが、deprecatedになりました。

## データクラスからJsonにする場合

### Moshiの場合

```kotlin
val json = adapter.toJson(
    QiitaUserEntity(
        description = "description",
        profileImageUrl = "https://pbs.twimg.com/profile_images/1201406146822557696/ewFFvnAa_400x400.jpg",
        id = "001",
        itemsCount = null
    )
)
```

※adapterは共通です

### kotlinx.serializationの場合

```kotlin
val json = Json.encodeToString(
    QiitaUserEntity(
        description = "description",
        profileImageUrl = "https://pbs.twimg.com/profile_images/1201406146822557696/ewFFvnAa_400x400.jpg",
        id = "001",
        itemsCount = null
    )
)
```

`encodeFromString`を呼び出します。
`decodeFromString`と命名上対になっている感じがして自分はこちらのほうが好みかなと思いました。

# おわりに

今回は、Moshiとkotlinx.serializationの環境導入と実際に実装方法を比べてみました。

以下自分の所感です

・APIから呼び出すときはまだまだ`Moshi`の方が手軽かな？という印象を受けましたが、`serialization`の方は`conf`からきちんと設定する分しっかりとルールを決めておきたい人にはおすすめです

・`data class <-> json`の変換に関しては`serialization`のほうが命名上好きです

まだ、1.0.0-RCということで今後のバージョンアップでさらに便利になりそうな気がしています！
この際にMoshiから乗り換えてみてはいかがでしょうか。

もし全体的にどう設定したか知りたいという場合は、Moshiからserializationに移行するdiffをPRとして自分のRepositoryにて作ってみましたので、よろしければ下のリンクからどう設定したかご覧いただければと思います。
https://github.com/sudo5in5k/PagingSandBox/pull/3/files