1. はじめに
これまで個人の学習用プロジェクトでは、JSONのパースに Moshi、Navigationには 文字列ルート + sealed class という構成を使っていました。動作自体に問題はなかったのですが、画面を追加するたびに "detail/{id}" のような文字列ルートと navArgument のボイラープレートを繰り返し書く必要がある、という不満がありました。
現在の業務プロジェクトではすでに kotlinx.serialization を採用しており、実際に使ってみると Navigation Compose 2.8+ の Type-Safe Navigation まで同じライブラリで自然に統合できる点が印象的でした。そこで今回、学習用プロジェクトにも同じ構成を反映し、JSONパースとNavigationを1つのライブラリに統一するマイグレーションを行いました。
この記事では、次の2点を整理します。
- なぜ最近は Kotlinx Serialization が使われるのか(Gson・Moshiとの比較)
- 実プロジェクトで Moshi → Kotlinx Serialization、文字列ルート → Type-Safe Navigation へ移行した過程
2. Gson / Moshi / Kotlinx Serialization は何が違うのか
AndroidのJSONパースライブラリは大きく3つが使われてきました。それぞれの特徴を先に押さえておくと、なぜ今 kotlinx.serialization なのかが自然に理解できます。
1) Gson(2008〜)
Googleが作ったライブラリで、Android初期から標準のように使われてきました。
data class User(val id: Int, val name: String)
val gson = Gson()
val user = gson.fromJson(json, User::class.java)
課題
- リフレクションベース — 実行時にクラス情報を読み取ってフィールドを埋めるため遅く、R8/ProGuard使用時はルールの追加が必須
-
Kotlinのnull safetyを無視 —
val name: String(non-null)でもJSONにnameが無ければnullが入ってしまう -
Kotlinのデフォルト値を無視 —
val age: Int = 20と宣言してもJSONに無ければ0が入る -
コンストラクタを呼ばない — リフレクションでオブジェクトを生成してフィールドを埋めるだけなので、
initブロックの検証ロジックが走らない
2) Moshi(2016〜)
Square(Retrofitの開発元)が作ったライブラリ。GsonのKotlin周りの弱点を補うために登場しました。
@JsonClass(generateAdapter = true)
data class User(val id: Int, val name: String)
改善点
- Kotlinのnull safetyを認識 — non-nullフィールドが欠けているとパース失敗として明示的にエラーになる
- コード生成(KSP)方式に対応 — リフレクション無しでコンパイル時にアダプタを生成 → 高速でR8フレンドリー
- デフォルト値を尊重 — Kotlinのコンストラクタのデフォルト値が正しく使われる
残る課題
-
デフォルトモードは依然としてリフレクションベース —
KotlinJsonAdapterFactoryを使うとGson同様kotlin-reflectに依存する。リフレクションを避けるには@JsonClass(generateAdapter = true)+ KSPのコード生成モードを別途設定する必要がある - 追加の依存関係とKSPプロセッサが必要(
moshi-core、moshi-kotlin、moshi-kotlin-codegen) - JVM専用 — Android/Java環境でしか動かない
3) Kotlinx Serialization(2020〜)
JetBrainsが自ら開発したKotlin公式ライブラリです。アノテーションプロセッサではなく、Kotlinコンパイラプラグインとして動作します。
@Serializable
data class User(val id: Int, val name: String)
主な差別化ポイント
- リフレクションでもKSPでもない — コンパイラが直接シリアライズコードを生成する。最も速く、軽い
- Kotlin Multiplatform対応 — Android、iOS、どこでも同じように動作する
- Kotlin公式 — 言語機能(sealed class、value class、デフォルト値など)と100%互換
- Navigation Compose 2.8+ のType-Safe Navigationがこのライブラリを使用 → ルーティングとAPIパースを1つのライブラリに統合できる
📊 ひと目で比較
| 項目 | Gson | Moshi | Kotlinx Serialization |
|---|---|---|---|
| リリース | 2008 | 2016 | 2020 |
| 開発元 | Square | JetBrains(Kotlin公式) | |
| 動作方式 | リフレクション | リフレクション または KSPコード生成 | コンパイラプラグイン |
| Kotlin null safety | ❌ | ✅ | ✅ |
| Kotlinデフォルト値 | ❌ | ✅ | ✅ |
| sealed class対応 | ❌ | △(アダプタを自作) | ✅(ネイティブ) |
| Multiplatform | ❌(JVM専用) | ❌(JVM専用) | ✅ |
| R8 / ProGuardフレンドリー | ❌(ルール必要) | △(codegenモードのみ) | ✅ |
| Navigation Compose連携 | ❌ | ❌ | ✅ |
🎯 なぜ最近はKotlinx Serializationなのか
整理すると、理由は次の3点に集約されます。
- Kotlin公式ライブラリ — 言語機能と常に同期し、JetBrainsがメンテナンスしている
- コンパイラプラグイン方式 — リフレクションでもKSPでもない、効率的なコード生成方式
- エコシステムの広がり — Navigation ComposeやKtorなどがKotlinx Serializationを標準として採用。1つのライブラリでより広い領域をカバーできる
3. Before — Moshi + 文字列ルート
(1) Moshiの設定
@Provides
@Singleton
fun provideMoshi(): Moshi =
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
@Provides
@Singleton
fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("...")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(okHttpClient)
.build()
(2) 文字列ベースのNavigation
sealed class Screen(val route: String) {
data object Home : Screen("home")
data object Detail : Screen("detail/{id}") {
fun createRoute(id: Int) = "detail/$id"
const val ID_ARG = "id"
}
}
composable(
route = Screen.Detail.route,
arguments = listOf(
navArgument(Screen.Detail.ID_ARG) { type = NavType.IntType }
)
) { backStackEntry ->
val id = backStackEntry.arguments?.getInt(Screen.Detail.ID_ARG) ?: return@composable
DetailScreen(id = id, ...)
}
このコードの問題点
-
"detail/{id}"のような文字列ルートはタイプミスをしてもコンパイラが検出してくれない -
navArgument+NavTypeのボイラープレートが画面ごとに繰り返される - パラメータ取得時に
arguments?.getInt(...) ?: returnのようなnullable処理が必要
4. After — Kotlinx Serializationの適用
(1) Gradle依存関係の変更
[versions]
kotlinx-serialization = "1.7.3"
[libraries]
retrofit-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
plugins {
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit.converter.kotlinx.serialization)
}
(2) DTOに @Serializable を付与
@Serializable
data class MessageDto(
val id: Int,
val title: String,
// ...
)
(3) Retrofit Converterの差し替え
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("...")
.addConverterFactory(
Json { ignoreUnknownKeys = true }
.asConverterFactory("application/json; charset=UTF-8".toMediaType())
)
.client(okHttpClient)
.build()
MoshiインスタンスのProviderが不要になり NetworkModule がかなり軽くなりました。また ignoreUnknownKeys = true を指定することで、サーバー側で新しいフィールドが追加されてもアプリが壊れない防御的な実装にできます。
ignoreUnknownKeys はデフォルトで false です。未知のキーが来ると例外になるため、実サービスのAPIを扱う場合は明示的に true にしておくのが無難です。
(4) Type-Safe Navigation
@Serializable
data object Home
@Serializable
data class Detail(val id: Int)
NavHost(navController = navController, startDestination = Home) {
composable<Home> {
HomeScreen(onItemClick = { id -> navController.navigate(Detail(id)) })
}
composable<Detail> { backStackEntry ->
val detail: Detail = backStackEntry.toRoute()
DetailScreen(id = detail.id, ...)
}
}
@Serializable を付けたクラス自体がルートになり、navArgument、NavType、文字列ヘルパーがすべて不要になりました。
5. 何が良くなったか
| 項目 | Before | After |
|---|---|---|
| JSONパースライブラリ | Moshi | Kotlinx Serialization |
| コンパイラサポート | KSPによるアダプタ生成 | コンパイラプラグイン |
| Navigationルート | 文字列("detail/{id}") |
型安全なオブジェクト(Detail(id)) |
| パラメータ取得 | arguments?.getInt(...) |
backStackEntry.toRoute() |
Routes.kt のコード量 |
38行 | 27行(-29%) |
特にNavigation側の変化が体感として一番大きかったです。ルート定義 → 登録 → パラメータ取得までがすべて1つの型安全なオブジェクトで完結するため、新しい画面を追加するときに気にすることが半分になりました。
6. おわりに
JSONパースとNavigationは一見無関係に見えますが、どちらも 「オブジェクト ↔ 文字列」のシリアライズという共通点があります。Kotlinx Serializationを導入することで、この2つの領域のシリアライズ方式を1つのライブラリに統一でき、結果としてコード量が減り、型安全性が上がりました。
特にNavigation Compose 2.8+ のType-Safe NavigationはKotlinx Serializationを前提に設計されているため、新規プロジェクトであれば最初からKotlinx Serializationで始めるのが一番きれいな選択だと感じました。
実際のマイグレーションコミット: GEUN-TAE-KIM/Mvi_Orbit_Study @ 41ca94f