k
Gsonからkotlin serializationへの移行
アプリチームのKです。Android版ニコニコ漫画 / 読書メーターの開発を担当しています。 ニコニコ漫画アプリではserialization / deserialization libraryとしてGsonを利用していましたが、kotlin-serializationへmigrationを行いました。 公式のドキュメントは豊富であり、また漫画アプリでは複雑なモデルを利用していなかったこともありスムースに移行できたと思います。とは言え、ドキュメントが豊富なおかげで逆に必要な情報を見つけ出すのに多少苦労したことも事実ですし、またstable版がリリースされて日が浅いこともあり一般の情報が少ないとも感じました。そこでこの記事ではGsonからのmigrationに着目し、移行方法、及び注意点について紹介していきたいと思います。
環境
記事中のコードは次の環境下で動作確認しています。
target | version |
---|---|
kotlin | 1.4.31 |
kotlin-serialization | 1.1.0 |
retrofit | 2.5.0 |
retrofit2-kotlinx-serialization-converter | 0.8.0 |
okhttp | 4.9.1 |
Gson | 2.7 |
Http clientとしてretrofit、レスポンスコンバーターとしてJake氏によるretrofit2-kotlinx-serialization-converterを利用しています。okhttpはMediaType生成用に4系を利用しています。
移行方法
- シリアル化対象クラスへ
@Serializable
を追加
+ @Serializable
class Color(val rgb: Int)
kotlin-serializationではシリアル化可能なクラスへ@Serializable
を付与する必要があります。このマークを元にpluginはserializerを生成します。これにより意図しないクラスが処理されることを防いでいるようです。なお、enum classであった場合には追加不要です。ただし、@SerialNameを持つ場合は必要となりますのでご注意ください。
- @SerializedNameを@SerialNameへ
class Project(
val name: String,
+ @SerialName("lang")
- @Serialized("lang")
val language: String)
フィールド名とシリアル名が異なる場合、@SerialNameを使用します。Gsonではプロパティ(メソッド)のみに付与可能でしたがkotlin-serializationではClassに対しても付与可能です。
- enumクラスの扱い
未定義のenum valueに対しての処理に違いがあります。次のようなクラス、及びserial valueがあるとします。
enum class Status { ACTIVE, INACTIVE, UNKNOWN }
// enum未定義のvalue
{'status': 'PENDING'}
この場合、Gsonではnullとして処理されますが、kotlin-serializationはdeserializeエラーとなります。その為、enumクラスに対しては基本的にvalue解決用のserializerを独自実装する必要があります。
@Serializer(with = StatusSerializer::class)
enum class Status { ACTIVE, INACTIVE, UNKNOWN }
object StatusSerializer : KSerializer<Status> {
override val descriptor: SerialDescriptor = StringDescriptor
override fun serialize(output: Encoder, obj: Status) {}
override fun deserialize(input: Decoder): Status {
// resolove value.
return Status.resolve(decoder.decodeString())
}
}
執筆時点においてはdefalut valueの設定機能は存在していませんが、要望としては多いようなので1近い将来実装されるかもしれません。
- non-null types
Gson with koltin環境においてよく知られている問題としてGsonがkotlinのnon-null typeを処理できない件があります。次のようなモデル、シリアル値があるとします。
data class Project(val name: String, val type: Int)
{"name": null, "type": 0}
nameプロパティはnon-null typeですが、シリアル値はnullとなっています。デシリアライズでエラーとなってくれそうですがGsonではエラーとなりません。これはGsonがリフレクションを利用し、言語ルールを回避できているためです。kotlin serializationではエラーとなります。
- オプション値の扱い
deserialization時、Gsonではmodelのプロパティが存在しシリアル値が存在しない場合、その値はオプション値として扱われます。次のケースではtype
はオプションとして扱われ、0(タイプに応じたデフォルト値)がセットされます。
data class Project(val name: String, val type: Int)
{"name": "kotlinx.serialization"}
kotlin-serializationで上記を処理する場合、デシリアライズエラーとなります。kotlin-serializationでオプション値を扱う場合は手動でデフォルト値をセットする必要があります。
+ @Serializable
data class Project(
val name: String,
- val type: Int
+ val type: Int = 0
)
{"name": "kotlinx.serialization"}
- json decode
Gson、kotlin-serializationそれぞれの標準的なdecode処理ですが次のような形になると思います。
Gson
fun <T> decodeData(json: String, typeReference: TypeToken<T>): T {
return gson.fromJson(json, type.getType());
}
kotlin-serialization
inline fun <reified T> decodeData(json: String): T {
return decodeFromString<T>(json)
}
kotlin-serializationはリフレクションに対応していません。コンパイル時点で型が定義されていることが必須となりますが、inline展開することでこの問題を回避しています。
collectionの場合
List
Gson
class TestJsonObject
object : TypeToken<List<TestJsonObject>>() {}
kotlin-serialization
@Serializable
class TestJsonObject
ListSerializer(TestJsonObject.serializer())
- deserialize時の不明キーの取り扱い
Gsonの場合、モデル未定義のシリアル値についてはデフォルトで無視されますが、kotlin-serializationではデフォルト設定ではdeserializeエラーとなります。次のコードを実行した場合、
@Serializable
data class Project(val name: String)
fun main() {
val format = Json()
val data = format.decodeFromString<Project>("""
{"name":"kotlinx.serialization","language":"Kotlin"}
""")
println(data)
}
次のエラーが発生します。
kotlinx.serialization.SerializationException: Strict JSON encountered unknown key: language
未定義のシリアル値を無視する場合、次のようにJsonオブジェクトにignoreUnknownKeysを設定する必要があります。
- val format = Json()
+ val format = Json { ignoreUnknownKeys = true }
- 構文解析
kotlin-serializationデフォルトのJsonパーサーは可能な限り仕様(RFC4627)に準拠しています。これはGsonに比べて厳格であることを意味し、結果、Gson利用時は発生していなかったパースエラーが発生する可能性があります(実際ニコニコ漫画アプリではエラーとなるケースがありました)。例としてタイプ不一致があります。次のようなケースです。
data class Project(val id: String)
{"id": 12345}
シリアル値は数値、デシリアライズするモデルのプロパティがStringであった場合、Gsonではエラーとなりませんがkolin-serializationでは次のエラーが発生します
kotlinx.serialization.json.internal.JsonDecodingException: String literal for key 'id' should be quoted.
isLenient
プロパティを利用することでJsonの制約を緩和することができます。
- val format = Json()
+ val format = Json { isLenient = true }
なお項番7, 8で紹介したjsonのパースに関しては公式のドキュメント、及びconfigのコードを確認することでより深い理解を得られると思います。
最後に
Gsonからのマイグレーションにフォーカスして移行方法・及び注意点を紹介してきました。リフレクションでなくなったことでコード量(アプリサイズ)の増加という影響はありますがその差はごく僅かであり、それ以上に享受できるパフォーマンス改善や曖昧性の解消等のメリットの方が大きいと思います。experimentalな機能も多く今後の拡張にも期待できますのでフォローし続けていきます。
宣伝
トリスタでは、本が好きなスマートフォンアプリエンジニアを募集しています。
-
最初の提案から3年経過していますが。https://github.com/Kotlin/kotlinx.serialization/issues/90
↩
k
アプリエンジニア