背景
先日
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を使用しています)
-
(おそらく任意)
Kotlin Code Migrations
を行う
1.4.0 install後に、右下に出てきたのでやっています
※環境によっては出てくるタイミングが違うかもしれません
- Android Studioを再起動する
Build.gradleの変更
./build.gradleの変更
- ext.kotlin_versionを1.4.0へ
- dependenciesに
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
を追加
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
を使うことになります
apply plugin: 'kotlinx-serialization' // 追加
dependencies {
def serialization_version = "1.0.0-RC"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version" // 追加
}
これで準備は整いました
実装
ここからはMoshiと比較して以下のケースを考えてみます
- Retrofitを介してデータをとってくる
- encodeする
- decodeする
Retrofitを使ってAPIからのResponseをデータクラスに変換する場合
環境
RetrofitとそのAdapterを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
でも変わりはありません
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はこのようになります
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の定義
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の定義
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の定義
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が発生します。
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
をセットしているのがわかるかと思います。
/**
* 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
には何が設定できるかというと、
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 (デフォルト)
もし、デフォルトの設定でも構わないという場合は、
Json.asConverterFactory
という形で記述することができます。このJsonは以下のDefaultを呼び出しています。
@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を定義します。
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