
はじめに
こんにちは、株式会社エブリーで Android アプリ開発を担当している岡田です。
弊社が提供する デリッシュキッチン の Android アプリでは、アプリの堅牢性向上とモダンな開発体験のための選択として、JSON パーサーを従来の Gson から Kotlin Serialization への移行を検討しています。
今回は弊社で行なっているイベント「挑戦WEEK」にて、Gson から Kotlin Serialization への移行を、Android のコードベース変更に限定して挑戦してみました。こちらについて、少しお話しさせていただければと思います。
弊社の挑戦WEEKの取り組みについては以下の記事をご覧ください!
Gson について
Android アプリの開発において、API との通信で受け取った JSON をデータクラスに変換する「JSON パース」は避けては通れない実装です。
デリッシュキッチン の Android アプリでは、長らく JSON パーサーとして Google 製の「Gson」を利用してきました。Gson は非常に歴史が長く、Android アプリ開発の黎明期からデファクトスタンダードとして広く使われており、Retrofit などのネットワークライブラリとも標準で連携しやすいという特徴があります。
長年アプリの通信基盤を支えてくれた Gson ですが、プロジェクトのフル Kotlin 化が進み、よりモダンな言語仕様を活用していく中で、実は Android アプリを開発する上でいくつかの大きな課題を抱えるようになっていました。
Gson の課題
Java 時代には非常に優秀だった Gson ですが、Kotlin で構成された現代のアプリにおいては、Kotlin の強みである言語仕様とコンフリクトを起こすケースが目立つようになってきました。
1. Null 安全が破壊されるリスク
Gson は内部でリフレクション(sun.misc.Unsafe など)を用いてインスタンスを生成します。そのため、Kotlin のデータクラスでプロパティを「非 Null(String など)」で定義していても、サーバーから返ってくる JSON 側にそのキーが存在しない場合、Gson は強制的に null を代入してしまいます。
これにより、Kotlin コンパイラが保証しているはずの「Null 安全」がランタイムで破壊され、アプリの思わぬところで NullPointerException を引き起こす原因となっていました。
2. デフォルト引数が無視される
Kotlin のデータクラスでは val isPremium: Boolean = false のようにデフォルト引数を設定できます。しかし、Gson はコンストラクタを経由せずにインスタンスを生成することがあるため、JSON に該当のキーが含まれていない場合、このデフォルト値が適用されません。結果として、意図しない型の初期値(Int なら 0、参照型なら null)が入ってしまうという問題がありました。
これらの挙動は、開発者が意図しない「不正な状態を持ったインスタンス」がアプリ内を回遊することを意味しており、結果として予期せぬクラッシュの温床になり得ます。
Kotlin Serialization について
最終的に、これらの課題を根本から解決するために、Kotlin 公式が提供しているシリアライズライブラリ「Kotlin Serialization(kotlinx.serialization)」へ移行を検討しています。
Kotlin Serialization は、コンパイル時にシリアライズ・デシリアライズのためのコードを自動生成する仕組みを持っています。実行時に重いリフレクションを行わないため、非常にモダンで Kotlin ライクな設計となっています。
このライブラリへ切り替えることで、以下のような大きな恩恵を受けることができます。
- 厳格な Null 安全の保証
非 Null として定義したプロパティに対して JSON に値が存在しない場合、強制的に Null を入れるのではなく、パース時に明確に例外(
SerializationException)を投げてくれます。これにより、不正なデータによる後続処理でのクラッシュを防ぐことができます。 - デフォルト値の完全なサポート JSON にキーが存在しない場合、Kotlin 側で定義したデフォルト引数が正しく適用されます。
- パフォーマンス向上とアプリサイズ削減 リフレクションに依存しないため、パース速度が向上します。また、ProGuard/R8 による最適化とも相性が良く、アプリのバイナリサイズの削減にも繋がります。
具体的な修正内容
実際に Gson から Kotlin Serialization へ移行するにあたり、行った具体的な修正内容をご紹介します。
1. Data Class の書き換え
Gson の @SerializedName アノテーションを、Kotlin Serialization の @SerialName に変更し、クラスに @Serializable アノテーションを付与します。
【従来の Gson での実装】
data class UserResponse( @SerializedName("id") val id: Long, @SerializedName("user_name") val userName: String, @SerializedName("profile_image_url") val profileImageUrl: String? )
【新しい Kotlin Serialization での実装】
@Serializable data class UserResponse( @SerialName("id") val id: Long, @SerialName("user_name") val userName: String, // サーバーからキーが送られてこない可能性がある場合はデフォルト値を設定 @SerialName("profile_image_url") val profileImageUrl: String? = null, @SerialName("is_premium") val isPremium: Boolean = false )
2. Retrofit の Converter の置き換え
API 通信に Retrofit を使用しているため、Gson の ConverterFactory を Kotlin Serialization 用のものへ差し替えました。
この際、サーバーからのレスポンスにおいて、アプリ側で定義していない未知のキーが含まれていてもパースエラーにならないよう、ignoreUnknownKeys = true を設定しています。
// Json パーサーの設定 val json = Json { ignoreUnknownKeys = true // 未知のキーを無視する coerceInputValues = true // null が来た場合にデフォルト値があればフォールバックする } val contentType = "application/json".toMediaType() val retrofit = Retrofit.Builder() .baseUrl("https://api.example.com/") // GsonConverterFactory.create() からの置き換え .addConverterFactory(json.asConverterFactory(contentType)) .build()
主にこれらの修正を、API レスポンスを受け取る全てのデータクラスと Retrofit クライアントに対して適用し、段階的に移行を進めました。
また他にも com.google.gson.internal.bind.util.ISO8601Utils を利用している箇所や、JsonUtil という Android アプリ側で Json を扱う際に使用するクラスの修正など、細かい修正も行いました。
総差分ファイル数はおよそ 500 ファイルと、大規模な改修になりました。
まとめと今後の課題
今回の改修で JSON パーサーを Kotlin Serialization に移行したことにより、Kotlin の言語仕様に沿った厳格な型安全性が担保されます。Android のコードベース上での堅牢性は大きく向上しました。
しかし、ライブラリが「厳格」になったからこそ直面する新たな課題もあります。 それは、サーバーからのレスポンス仕様(スキーマ)の正確な把握 です。
Gson の時代は「JSON にキーがなくても、とりあえず Null を入れてクラッシュさせない」という緩さがありました。しかしこれからは、非 Null プロパティのキーが JSON に存在しなければ、即座にパース失敗となってしまいます。
これを防ぐためには、以下のような対応をサービス全体で意識していく必要があります。
- サーバーレスポンスで Null が返る、またはキーが省略される可能性のあるフィールドには、適切な Nullable 定義やデフォルト値を設定する
- クラッシュログを監視し、パースエラーが発生した場合は迅速にデータクラスの定義をチューニングする
- サーバーサイドのエンジニアと密に連携し、API 仕様書とクライアント実装の乖離をなくす
デリッシュキッチンは歴史のあるサービスですから、型安全に API レスポンスをパースするには、この辺りの見直しは避けて通れません。
時間と根気がいる作業にはなりますが、徐々にでも整備できればと思います。
もしまだ Gson を利用している方で、「データクラスの Null 安全が担保できずに困っている」「原因不明の NullPointerException に悩まされている」と感じているなら、一度 JSON パーサーの移行を検討してみてはいかがでしょうか。
今後も、Kotlin の厳格な型安全性を武器に、より品質が高く安定した デリッシュキッチン をユーザーの皆様にお届けできるよう、改善を続けていきます。