LoginSignup
14
12

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-29

背景

先日
Kotlin1.4で使いたい新機能を一部紹介する
という記事を書かせていただいて、その中でkotlinx.serializatonが気になっていました。

JSONパーサのライブラリは色々ありますが、「Gson is deprecated.」からMoshiを使っている方が多いのではないでしょうか。

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

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

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

導入

今回はAndroid Studioでの導入を想定しています
他PFで導入方法を知りたい方はこちらを参照ください

Kotlinを1.4へ

  • Android Studioを最新版へ (Previewではない方が安定するとは思います、私は執筆時点では4.0.1を使用しています)
  • Languages & Frameworks -> Kotlinから1.4.0をinstall
    スクリーンショット 2020-08-29 2.47.33.png

  • (おそらく任意)Kotlin Code Migrationsを行う
    1.4.0 install後に、右下に出てきたのでやっています
    スクリーンショット 2020-08-29 2.52.56.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になる前に導入している方はdependencieskotlinx-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からユーザー一覧を取得することを考えてみます

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とは少し違うところです。

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

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

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.ktkotlin-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ファイル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の場合

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

kotlinx.serializationの場合

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

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

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

Moshiの場合

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の場合

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

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12