Razl-Dazl

eyecatch

JSONデシリアライザをGsonからKotlin Serializationに変えようとしたら色々つまづいた

Posted at — 2023-03-24

JSONこねくり回すライブラリをGsonしか知らなかったので、今までずっとGsonを使っていました

訳アリで変える必要が出てきたのでKotlin Serializationへ移行しました


Gsonで出来ないこと

まずはGson脱却理由をおはなしします

Gsonでは、JSON文字列をlazyプロパティを持つdata classへパースしようとすると、遅延初期化が実行されずにnullってしまいます

data class Person(
	val firstName: String,
	val lastName: String,
) {
	val name by lazy { "$firstName $lastName"}
}

上記のようなコードでnameプロパティにアクセスしてもnullってしまうのです

Gsonはオブジェクトの生成時に一旦空のコンストラクタを使用するようなのですが、このような空のコンストラクタが存在しないクラスにおいてもリフレクションなりゴニョゴニョして無理やり空のコンストラクタを使用するようで、そのあたりが原因っぽい・・・

原因についてはこちらに詳しい解説があります・・・

しかーし!Kotlin Serializationならlazyプロパティもちゃんと使えるのです! という訳で置き換えます

別にlazyである必要ないなと途中で気づいたんですけど、コンストラクタの引数を使って新たにプロパティを設ける場合にどっちにしろGsonだと上手くいかなかったので結局Gsonは脱却しました

いろいろゴチャゴチャ言ってますがKotlin純正の方が相性がいいので是非置き換えましょう


移行の注意点

具体的な使い方(ライブラリの導入とか)は他サイトにまかせて省略しますが注意点をいくつか上げておきます

主にGsonで適当に誤魔化していたものになります

(1) デフォルトだと制約がギチギチ

JSON文字列内に、定義済みdata classに存在しないフィールドが含まれる場合、デフォルトではデシリアライズ時に怒られます

@Serializable
data class Person(
	val first_name: String,
	val last_name: String,
)

こういうクラスを定義して

{
	"first_name": "noa",
    "last_name": "ushio",
    "age": 16
}

上記のようなJSON文字列があった時に

Json.decodeFromString(Person.serializer(), str)

ただそのままデシリアライズするとdata classにageプロパティが存在しないがために蹴られます

val json = Json { ignoreUnknownKeys = true }
json.decodeFromString(Person.serializer(), str)

ラムダで設定値を渡してあげることで、存在しないフィールドを見逃すことができるようになります

Gsonのときはこういう細かい事は考えずに済んだのですが、Kotlin Serializationだとちょっとめんどくさい


(2) プロパティ名は完全に一致させる

まあさっきと同じサンプルなんですけど

{
	"first_name": "noa",
    "last_name": "ushio",
    "age": 16
}

こういう文字列をパースしたいときに

@Serializable
data class Person(
	val firstName: String,
	val lastName: String,
	val age: Int,
)

data classの定義がこんな感じだと上手くいきません

Gsonはアンダースコアを勝手に省略してくれましたがKotlin Setializationだと考慮してくれないので、ちゃんとval first_nameとかval last_nameみたいに書きましょう・・・

@Serializable
data class Person(
	val first_name: String,
	val last_name: String,
	val age: Int,
)

これだとちゃんと動く

なんか設定値変えてこの辺考慮するようにできたらいいんですが・・・


(3) プロパティがNullableでもデシリアライズに失敗するケース

JSON文字列内にそもそもフィールドが存在しない場合、data classのプロパティがnullableだったとしてもコケます

{
	"first_name": "noa",
    "last_name": "ushio",
}

こういうJSON文字列があったときに

@Serializable
data class Person(
	val firstName: String,
	val lastName: String,
	val age: Int?,
)

とnullableでプロパティを定義しただけではageの値が補完されません

プロパティのNull許容とは分けて考える必要があります

@Serializable
data class Person(
	val firstName: String,
	val lastName: String,
	val age: Int? = null,
)

フィールドが無いケースを考慮するには、このようにdata classの方でデフォルト値を定義しておく必要があります


自分がハマった点は以上です

このあたりの細かい設定値などの説明は、公式のドキュメントが充実しているので合わせてどうぞ(というか参考にした)

Author@zakuro

Mastodon: 396@vivaldi.net