Kotlin Serialization入門:JSONデータ変換の基本

Kotlin Serialization入門:JSONデータ変換の基本

はじめに

現代のソフトウェア開発において、異なるシステム間でのデータ交換は不可欠です。その中でも、JSON(JavaScript Object Notation)は軽量で人間が読み書きしやすく、機械にとってもパースが容易であることから、REST API、設定ファイル、データの永続化など、多岐にわたる用途で広く利用されています。

Kotlinは、その簡潔さと強力な機能により、サーバーサイドからAndroid、デスクトップ、さらにはWebAssemblyまで、様々なプラットフォームで人気を博しています。KotlinでJSONデータを扱う際、手動でJSON文字列をパースしたり組み立てたりするのは煩雑であり、エラーも発生しやすいため、通常は専門のライブラリを利用します。

Kotlinには、Jackson、Gson、Moshiなど、Javaの世界で広く使われているJSONライブラリをKotlinから利用することも可能ですが、JetBrainsが開発・メンテナンスしている公式ライブラリである「Kotlin Serialization」は、Kotlin言語の特性を最大限に活かすように設計されており、特にKotlinプロジェクトでのJSON変換において非常に優れた選択肢となります。

Kotlin Serializationとは?

Kotlin Serializationは、Kotlinオブジェクトと様々なデータフォーマット(JSON, Protocol Buffers, CBORなど)との間の双方向の変換(シリアライズとデシリアライズ)を、コンパイル時に生成されるコードによって行うライブラリです。他の多くのライブラリが実行時のリフレクションを利用するのに対し、Kotlin Serializationはコンパイル時に必要な処理を生成するため、リフレクションベースのライブラリと比較して一般的にパフォーマンスが優れており、かつリフレクションが利用できないKotlin/Nativeのような環境でも利用可能です。

なぜKotlin Serializationを使うのか?

  1. Kotlinとの親和性: データクラス、null安全性、コルーチンなど、Kotlinの言語機能とシームレスに統合されます。特に、null安全性を考慮した設計は、デシリアライズ時のエラーを防ぐのに役立ちます。
  2. コンパイル時の安全性: @Serializable アノテーションをつけたクラスは、コンパイル時にシリアライズ可能かどうかがチェックされます。問題があればコンパイルエラーとして早期に検出できるため、実行時エラーのリスクを減らせます。
  3. マルチプラットフォーム対応: Kotlin/JVM、Kotlin/JS、Kotlin/Nativeといったマルチプラットフォームプロジェクトで利用できます。これにより、共通コード内でデータ構造を定義し、異なるプラットフォーム間で共有することが容易になります。
  4. パフォーマンス: リフレクションを使用しないため、高速なシリアライズ/デシリアライズが可能です。
  5. 拡張性: JSONだけでなく、Protocol Buffers、CBOR、Propertiesなどの様々なフォーマットに対応しており、将来的にはカスタムフォーマットにも対応可能です。

この記事では、Kotlin Serializationを使ってKotlinオブジェクトとJSON文字列の間でデータを変換する基本的な方法に焦点を当てます。プロジェクトへの導入から、シリアライズ可能なクラスの定義、実際の変換方法、そしていくつかの便利な設定オプションまで、詳細に解説していきます。

対象読者

  • Kotlinを使ってJSONデータを扱いたいと考えているエンジニア。
  • 他のJSONライブラリからの移行を検討しているエンジニア。
  • Kotlin Serializationの基本的な使い方を学びたい方。

Kotlinの基本的な文法(データクラス、プロパティ、nullableなど)を理解していることを前提とします。

それでは、Kotlin Serializationの世界へ踏み込んでいきましょう。

準備:プロジェクトへの導入

Kotlin Serializationを使うためには、まずプロジェクトにライブラリとコンパイラプラグインを導入する必要があります。ここでは、最も一般的なビルドシステムであるGradle(Kotlin DSLおよびGroovy DSL)を使った導入方法を説明します。

Gradle (Kotlin DSL)

build.gradle.kts ファイルを開き、plugins ブロックと dependencies ブロックに以下の記述を追加します。

“`kotlin
// build.gradle.kts
plugins {
// 必ず適用する必要があるKotlinプラグイン
kotlin(“jvm”) version “1.9.22” // 使用しているKotlinのバージョンに合わせてください
// Kotlin Serializationプラグイン
kotlin(“plugin.serialization”) version “1.9.22” // 使用しているKotlinのバージョンに合わせてください
}

repositories {
// 依存関係を解決するためのリポジトリ
mavenCentral()
}

dependencies {
// Kotlin標準ライブラリ (通常は既に含まれています)
implementation(kotlin(“stdlib”))

// kotlinx-serialization-core: Serializationのコア機能
// kotlinx-serialization-json: JSON形式を扱うためのライブラリ
// 他のフォーマットを使いたい場合は、例えば kotlinx-serialization-cbor などを追加します。
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") // 最新バージョンを確認してください

}

// オプション: テスト用の依存関係
// testImplementation(kotlin(“test”))
“`

重要な点:

  • kotlin("plugin.serialization") プラグインを適用する必要があります。このプラグインは、@Serializable アノテーションが付与されたクラスに対して、シリアライズ/デシリアライズに必要なコードをコンパイル時に生成します。
  • kotlinx-serialization-json は、JSONフォーマットを扱うための具体的な実装を提供します。他のフォーマット(CBOR, Protocol Buffersなど)を使いたい場合は、対応するライブラリを追加します。
  • バージョン番号は、執筆時点での最新バージョン(Kotlin 1.9.22, kotlinx-serialization-json 1.6.2)を例としています。ご利用の環境や最新版に合わせて適宜変更してください。KotlinプラグインとSerializationプラグインのバージョンは、通常同じか互換性のあるバージョンを使用します。kotlinx-serialization-json のバージョンは、Serializationプラグインのバージョンと互換性がある必要があります。通常、最新版を使用すれば問題ありません。

Gradle (Groovy DSL)

build.gradle ファイルを開き、plugins ブロックと dependencies ブロックに以下の記述を追加します。

“`groovy
// build.gradle
plugins {
// 必ず適用する必要があるKotlinプラグイン
id ‘org.jetbrains.kotlin.jvm’ version ‘1.9.22’ // 使用しているKotlinのバージョンに合わせてください
// Kotlin Serializationプラグイン
id ‘org.jetbrains.kotlin.plugin.serialization’ version ‘1.9.22’ // 使用しているKotlinのバージョンに合わせてください
}

repositories {
// 依存関係を解決するためのリポジトリ
mavenCentral()
}

dependencies {
// Kotlin標準ライブラリ (通常は既に含まれています)
implementation “org.jetbrains.kotlin:kotlin-stdlib”

// kotlinx-serialization-core: Serializationのコア機能
// kotlinx-serialization-json: JSON形式を扱うためのライブラリ
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" // 最新バージョンを確認してください

}

// オプション: テスト用の依存関係
// testImplementation “org.jetbrains.kotlin:kotlin-test”
“`

どちらのDSLを使用している場合でも、これらの変更を加えた後、Gradleプロジェクトを同期(Reload Project)することを忘れないでください。これにより、必要なライブラリがダウンロードされ、プラグインが適用されます。

これで、KotlinプロジェクトでKotlin Serializationを利用するための準備が整いました。次に、実際にシリアライズ可能なクラスを定義する方法を見ていきましょう。

基本的な使い方:シリアライズ可能なクラスの定義

Kotlin Serializationを使ってJSONとオブジェクトを相互変換するには、まず変換したいKotlinクラスを「シリアライズ可能」としてマークする必要があります。これには @Serializable アノテーションを使用します。

@Serializable アノテーションをクラスに付与すると、Kotlin Serializationコンパイラプラグインがそのクラスのシリアライズとデシリアライズに必要なコードを自動的に生成します。これにより、開発者が手動で変換ロジックを書く手間が省け、エラーの可能性も減ります。

データクラスと@Serializable

Kotlinのデータクラスは、主にデータを保持するために設計されており、ボイラープレートコード(equals(), hashCode(), toString(), copy()など)を自動生成してくれるため、シリアライズ可能なデータ構造を定義するのに非常に適しています。@Serializable アノテーションは、データクラスと組み合わせて使うのが一般的です。

例1:シンプルなデータクラス

ユーザー情報を表すシンプルなデータクラスを定義してみましょう。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
data class User(
val name: String,
val age: Int
)
“`

この User クラスは、name (String) と age (Int) という2つのプロパティを持っています。@Serializable アノテーションが付いているため、このクラスのオブジェクトはJSONにシリアライズしたり、JSONからデシリアライズしたりできるようになります。

サポートされるプロパティ型

Kotlin Serializationは、以下の基本的な型を標準でサポートしています。

  • プリミティブ型: Byte, Short, Int, Long, Float, Double, Char, Boolean
  • String: String
  • 配列型: ByteArray, ShortArray, IntArray, LongArray, FloatArray, DoubleArray, CharArray, BooleanArray, Array<T>
  • 標準ライブラリのコレクション型: List<T>, Set<T>, Map<K, V>, Collection<T> (ただし、これらのインターフェースは直接はシリアライズできません。具象クラスである ArrayList, HashSet, HashMap などを使うか、あるいはインターフェース型を使う場合は、ジェネリック型引数 T なども @Serializable である必要があります。)
  • Pair<A, B>, Triple<A, B, C>
  • Result<T>
  • Enumクラス
  • オブジェクト宣言 (object)
  • @Serializable アノテーションが付与されたカスタムクラス(データクラスを含む)

これらの型をプロパティに持つクラスであれば、そのクラス自体に @Serializable を付けるだけでシリアライズ可能になります。

例2:コレクションを含むデータクラス

記事情報を表すデータクラスで、タグのリストを含む場合を考えます。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
data class Article(
val title: String,
val tags: List
)
“`

tags プロパティは List<String> 型です。ListString はいずれもKotlin Serializationがサポートする型なので、Article クラス全体に @Serializable を付けるだけでOKです。

例3:入れ子になったデータクラス

会社とその住所を表すデータクラスのように、データ構造が入れ子になっている場合もよくあります。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
data class Company(
val name: String,
val address: Address
)

@Serializable
data class Address(
val city: String,
val zip: String
)
“`

Company クラスの address プロパティは Address 型です。Address クラス自体にも @Serializable アノテーションが付いている必要があることに注意してください。このように、プロパティの型がカスタムクラスである場合は、そのカスタムクラスも @Serializable である必要があります。

Nullableプロパティの扱い

Kotlinのnull安全性は、データクラスの定義にも反映されます。Kotlin Serializationは、nullableなプロパティ (? 付きの型) を適切に扱います。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
data class Person(
val name: String,
val email: String? // null許容なプロパティ
)
“`

email プロパティはnull許容です。

  • Kotlinオブジェクトの emailnull の場合、デフォルトではJSON出力にそのキー("email")は含まれません。
  • JSON文字列に "email" キーが含まれていない場合、デシリアライズされたKotlinオブジェクトの email プロパティは null になります。
  • JSON文字列に "email": null のように null 値が明示的に含まれている場合、デシリアライズされたKotlinオブジェクトの email プロパティは null になります。

このデフォルトの挙動は、JSONパーサーの一般的な慣習(キーの欠落はnullを意味する)に沿っており、非常に便利です。ただし、後述する Json オブジェクトの設定で、null値をJSONに明示的に出力させることも可能です。

デフォルト値と@EncodeDefault

データクラスのプロパティにデフォルト値を設定することができます。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
data class Product(
val id: Int,
val name: String = “Unknown Product”, // デフォルト値を持つプロパティ
val price: Double = 0.0 // デフォルト値を持つプロパティ
)
“`

nameprice プロパティにはデフォルト値が設定されています。Kotlin Serializationのデフォルトの挙動では、プロパティの値がデフォルト値と同じである場合、そのプロパティはJSON出力に含まれません。これはJSONをコンパクトにするのに役立ちます。

例:
kotlin
val product = Product(id = 1) // nameは"Unknown Product", priceは0.0になる
// シリアライズすると、デフォルトでは id だけがJSONに含まれる
// {"id": 1}

デフォルト値を持つプロパティでも、値がデフォルト値と異なる場合は通常通りJSONに含まれます。

例:
kotlin
val product = Product(id = 2, name = "Laptop", price = 1200.0)
// シリアライズすると
// {"id": 2, "name": "Laptop", "price": 1200.0}

@EncodeDefault アノテーション

もし、プロパティの値がデフォルト値と同じであっても、常に JSON出力に含めたい場合は、@EncodeDefault アノテーションを使用します。

“`kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.EncodeDefault

@Serializable
data class ProductWithDefaults(
val id: Int,
@EncodeDefault // デフォルト値でもエンコードする
val name: String = “Unknown Product”,
val price: Double = 0.0 // デフォルト値の場合はエンコードしない (デフォルトの挙動)
)
“`

例:
kotlin
val product = ProductWithDefaults(id = 1) // nameは"Unknown Product", priceは0.0になる
// シリアライズすると
// {"id": 1, "name": "Unknown Product"} // nameはデフォルト値だがEncodeDefaultで出力される

price@EncodeDefault が付いていないため、デフォルト値の場合は出力されません。

この name プロパティの例のように、デフォルト値が文字列リテラルの場合は @EncodeDefault アノテーションが必要です。数値型や真偽値など、プリミティブ型のデフォルト値については、Json オブジェクトの設定 (encodeDefaults = true) を使用することも可能ですが、プロパティごとに制御したい場合は @EncodeDefault が便利です。通常はデフォルトの挙動(デフォルト値ならスキップ)で十分なことが多いでしょう。

オブジェクト宣言 (object) のシリアライズ

Kotlinのオブジェクト宣言 (object) もシリアライズ可能です。これは、シングルトンオブジェクトをJSONで表現したい場合に便利です。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
object Config {
val version: Int = 1
val apiKey: String = “abcde12345”
}
“`

この Config オブジェクトをシリアライズすると、そのプロパティを持つJSONオブジェクトとして出力されます。

json
{
"version": 1,
"apiKey": "abcde12345"
}

デシリアライズ時には、既存のシングルトンインスタンスが返されます。

Enumクラスのシリアライズ

Enumクラスも @Serializable を付けることでシリアライズ可能になります。デフォルトでは、enum定数の名前(文字列)としてシリアライズされます。

“`kotlin
import kotlinx.serialization.Serializable

@Serializable
enum class Status {
ACTIVE,
INACTIVE,
PENDING
}

@Serializable
data class Task(
val id: Int,
val status: Status
)
“`

例:
kotlin
val task = Task(id = 101, status = Status.ACTIVE)
// シリアライズすると
// {"id": 101, "status": "ACTIVE"}

JSONからデシリアライズする際は、対応するenum定数に変換されます。JSON文字列のenum名が大文字・小文字を区別するかどうかは、後述の Json オブジェクトの設定で制御できます。

継承と多態性 (簡易説明)

Kotlin Serializationは、sealedクラスを含む継承関係にあるクラスのシリアライズもサポートしています。ただし、これには追加の設定(@Subtype やポリモーフィックシリアライザーの設定)が必要です。これは「基本」の範囲を超えるため、この記事では詳細を割愛しますが、継承を利用したデータ構造をシリアライズ・デシリアライズしたい場合は、公式ドキュメントの「Polymorphic serialization」のセクションを参照してください。

基本的なデータ構造であれば、上記のデータクラス、コレクション、Nullable、デフォルト値、Enum、Object宣言を組み合わせることで、ほとんどのJSON構造に対応できます。

これで、シリアライズ可能なKotlinクラスを定義する方法が理解できました。次は、定義したクラスを使って実際にJSONとの間で変換を行う方法を見ていきましょう。

JSONとの相互変換:シリアライズとデシリアライズ

シリアライズ可能なクラスを定義したら、いよいよKotlinオブジェクトとJSON文字列の間で変換を行います。この変換を行うために、kotlinx-serialization-json ライブラリが提供する Json オブジェクトを使用します。

Json オブジェクトは、JSON形式でのシリアライズ/デシリアライズを実行するための主要なインターフェースを提供します。標準的な変換には、デフォルト設定の Json オブジェクトを利用するのが最も簡単です。

Json オブジェクトの生成

デフォルト設定の Json オブジェクトは、Json クラスのコンパニオンオブジェクトを通じて取得できます。

“`kotlin
import kotlinx.serialization.json.Json

// デフォルト設定のJsonオブジェクト
val json = Json
“`

この json オブジェクトを使って、以降の変換処理を行います。

シリアライズ (Kotlinオブジェクト -> JSON文字列)

KotlinオブジェクトをJSON文字列に変換することを「シリアライズ」と呼びます。これには Json.encodeToString() メソッドを使用します。

encodeToString() メソッドは、serializer<T>() から取得できる型 T のシリアライザーと、シリアライズしたいオブジェクトを引数に取ります。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString // encodeToString はこのパッケージにあります

@Serializable
data class User(val name: String, val age: Int)

fun main() {
val user = User(“Alice”, 30)

// UserオブジェクトをJSON文字列にシリアライズ
val jsonString = Json.encodeToString(user) // コンパイル時型推論によりserializer<User>()は省略可能

println(jsonString)
// 出力例: {"name":"Alice","age":30}

}
“`

Json.encodeToString(user) は、user オブジェクトの型(User)を推論し、対応するシリアライザーを使ってオブジェクトをJSON文字列に変換します。生成されるJSON文字列は、プロパティ名をキーとし、プロパティの値を値としたJSONオブジェクト形式になります。

例:コレクションのシリアライズ

リストやマップなどのコレクションも、要素の型がシリアライズ可能であれば、そのままシリアライズできます。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.builtins.ListSerializer // コレクションのシリアライザーを明示的に指定する場合

@Serializable
data class Article(val title: String, val tags: List)

fun main() {
val article = Article(“Kotlin Serialization Tutorial”, listOf(“Kotlin”, “Serialization”, “JSON”))

// ArticleオブジェクトをJSON文字列にシリアライズ
val jsonStringArticle = Json.encodeToString(article)
println(jsonStringArticle)
// 出力例: {"title":"Kotlin Serialization Tutorial","tags":["Kotlin","Serialization","JSON"]}

// Stringのリストを直接シリアライズ
val tagList = listOf("Kotlin", "Serialization", "JSON")
val jsonStringList = Json.encodeToString(tagList) // List<String>のserializerが推論される
println(jsonStringList)
// 出力例: ["Kotlin","Serialization","JSON"]

// もしくは、ジェネリック型を持つコレクションの場合、serializer を明示的に指定することもできます(通常は推論で十分ですが、複雑なケースで必要になることがあります)。
// val jsonStringListExplicit = Json.encodeToString(ListSerializer(String.serializer()), tagList)
// println(jsonStringListExplicit)

}
“`

Json.encodeToString(tagList) のように、コレクションを直接シリアライズすることも可能です。この場合、出力されるJSONはJSON配列になります。

例:Nullableプロパティとデフォルト値のシリアライズ

前述のNullableプロパティとデフォルト値を持つクラスのシリアライズ例です。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.EncodeDefault

@Serializable
data class Person(val name: String, val email: String?)

@Serializable
data class ProductWithDefaults(
val id: Int,
@EncodeDefault
val name: String = “Unknown Product”,
val price: Double = 0.0
)

fun main() {
// Nullableプロパティの例
val personWithEmail = Person(“Bob”, “[email protected]”)
val personWithoutEmail = Person(“Charlie”, null)

println("Person with email: ${Json.encodeToString(personWithEmail)}")
// 出力例: Person with email: {"name":"Bob","email":"[email protected]"}

println("Person without email: ${Json.encodeToString(personWithoutEmail)}")
// 出力例: Person without email: {"name":"Charlie"} // emailキーは含まれない

// デフォルト値の例
val productDefault = ProductWithDefaults(id = 1)
val productCustom = ProductWithDefaults(id = 2, name = "Mouse", price = 25.50)

println("Product with default values: ${Json.encodeToString(productDefault)}")
// 出力例: Product with default values: {"id":1,"name":"Unknown Product"} // @EncodeDefaultのおかげでnameは出力される
                                                                            // priceはデフォルト値なので出力されない

println("Product with custom values: ${Json.encodeToString(productCustom)}")
// 出力例: Product with custom values: {"id":2,"name":"Mouse","price":25.5}

}
“`

このように、デフォルト設定の Json オブジェクトは、Kotlinのnull安全性やデフォルト値の特性を考慮してJSONを生成します。

デシリアライズ (JSON文字列 -> Kotlinオブジェクト)

JSON文字列をKotlinオブジェクトに変換することを「デシリアライズ」と呼びます。これには Json.decodeFromString() メソッドを使用します。

decodeFromString() メソッドは、デシリアライズ先の型 T のシリアライザーと、JSON文字列を引数に取ります。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString // decodeFromString はこのパッケージにあります

@Serializable
data class User(val name: String, val age: Int)

fun main() {
val jsonString = “””{“name”:”Alice”,”age”:30}””” // 変換元のJSON文字列

// JSON文字列をUserオブジェクトにデシリアライズ
val user = Json.decodeFromString<User>(jsonString) // 型引数 <User> が必要

println("Name: ${user.name}, Age: ${user.age}")
// 出力: Name: Alice, Age: 30

// デシリアライズされたオブジェクトが元のオブジェクトと一致するか確認 (equals)
val originalUser = User("Alice", 30)
println("Objects are equal: ${user == originalUser}")
// 出力: Objects are equal: true

}
“`

Json.decodeFromString<User>(jsonString) は、JSON文字列をパースし、その内容から User クラスのインスタンスを生成します。decodeFromString の場合は、デシリアライズしたい型を型引数 <User> として明示的に指定する必要があります。

例:コレクションのデシリアライズ

JSON配列をKotlinのコレクションにデシリアライズする場合も同様です。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.builtins.ListSerializer

@Serializable
data class Article(val title: String, val tags: List)

fun main() {
// JSON文字列からArticleオブジェクトにデシリアライズ
val jsonStringArticle = “””{“title”:”Kotlin Serialization Tutorial”,”tags”:[“Kotlin”,”Serialization”,”JSON”]}”””
val article = Json.decodeFromString

(jsonStringArticle)
println(“Article title: ${article.title}”)
println(“Article tags: ${article.tags}”)
// 出力:
// Article title: Kotlin Serialization Tutorial
// Article tags: [Kotlin, Serialization, JSON]

// JSON配列からStringのリストにデシリアライズ
val jsonStringList = """["Kotlin","Serialization","JSON"]"""
val tagList = Json.decodeFromString<List<String>>(jsonStringList) // 型引数 <List<String>> が必要
println("Tag list: $tagList")
// 出力: Tag list: [Kotlin, Serialization, JSON]

// ジェネリック型を持つコレクションの場合、serializer を明示的に指定することもできます(通常は推論で十分ですが、複雑なケースで必要になることがあります)。
// val tagListExplicit = Json.decodeFromString(ListSerializer(String.serializer()), jsonStringList)
// println("Tag list (explicit): $tagListExplicit")

}
“`

コレクションをデシリアライズする場合も、デシリアライズ先の型を正確に指定することが重要です。List<String> のように、要素の型も含めて指定します。

例:Nullableプロパティのデシリアライズ

Nullableプロパティを持つクラスへのデシリアライズは、JSON文字列にそのキーが存在するかどうかによって挙動が変わります。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString

@Serializable
data class Person(val name: String, val email: String?)

fun main() {
val jsonWithEmail = “””{“name”:”Bob”,”email”:”[email protected]”}”””
val personWithEmail = Json.decodeFromString(jsonWithEmail)
println(“Person with email: Name=${personWithEmail.name}, Email=${personWithEmail.email}”)
// 出力: Person with email: Name=Bob, [email protected]

val jsonWithoutEmail = """{"name":"Charlie"}""" // emailキーが存在しない
val personWithoutEmail = Json.decodeFromString<Person>(jsonWithoutEmail)
println("Person without email: Name=${personWithoutEmail.name}, Email=${personWithoutEmail.email}")
// 出力: Person without email: Name=Charlie, Email=null // emailプロパティはnullになる

val jsonWithExplicitNullEmail = """{"name":"David","email":null}""" // emailキーがnull
 val personWithExplicitNullEmail = Json.decodeFromString<Person>(jsonWithExplicitNullEmail)
println("Person with explicit null email: Name=${personWithExplicitNullEmail.name}, Email=${personWithExplicitNullEmail.email}")
// 出力: Person with explicit null email: Name=David, Email=null // emailプロパティはnullになる

}
“`

JSON文字列にキーが存在しない場合、またはキーの値が null である場合、Kotlin Serializationは対応するnullableプロパティを null にデシリアライズします。これにより、JSONデータの欠落に対して堅牢なコードを書くことができます。非nullableなプロパティに対応するキーがJSONに存在しない場合は、デシリアライズ時にエラーが発生します(これについては後述のエラーハンドリングで詳しく説明します)。

Serializer の取得方法

encodeToStringdecodeFromString メソッドは、通常、型のシリアライザーを自動的に推論してくれます。しかし、ジェネリック型を持つ複雑なケース(例: List<MyCustomClass>, Map<String, AnotherClass>) や、ジェネリック型のパラメータを持つクラスの場合、シリアライザーを明示的に指定する必要があることがあります。

これには serializer<T>() 関数を使用します。

“`kotlin
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

@Serializable
data class Item(val id: Int, val name: String)

fun main() {
val itemList = listOf(Item(1, “A”), Item(2, “B”))

// List<Item> のシリアライザーを取得
val itemListSerializer: KSerializer<List<Item>> = ListSerializer(Item.serializer())

// 明示的にシリアライザーを指定してシリアライズ
val jsonStringList = Json.encodeToString(itemListSerializer, itemList)
println(jsonStringList)
// 出力: [{"id":1,"name":"A"},{"id":2,"name":"B"}]

// JSON文字列からList<Item>にデシリアライズ
val decodedItemList = Json.decodeFromString(itemListSerializer, jsonStringList)
println(decodedItemList)
// 出力: [Item(id=1, name=A), Item(id=2, name=B)]

// 通常は型推論で十分ですが、明示的に指定する必要がある場合にこの方法を使います。
// val jsonStringListInferred = Json.encodeToString(itemList) // こちらでも動くことが多い
// val decodedItemListInferred = Json.decodeFromString<List<Item>>(jsonStringList) // こちらでも動くことが多い

}
“`

serializer<Item>() は、@Serializable が付与された Item クラスのシリアライザーを返します。ListSerializer(Item.serializer()) は、「要素の型が Item であるリスト」のためのシリアライザーを構築します。このように、組み込みの *Serializer クラスと serializer<T>() 関数を組み合わせて、複雑な型のシリアライザーを取得できます。

これで、基本的なKotlinオブジェクトとJSON文字列の間のシリアライズ・デシリアライズの方法が分かりました。デフォルト設定の Json オブジェクトは多くのケースで機能しますが、JSONの形式が標準的でない場合や、特定の挙動をカスタマイズしたい場合があります。次に、Json オブジェクトの高度な設定オプションについて見ていきましょう。

高度な設定とカスタマイズ

Kotlin Serializationの Json オブジェクトは、様々なオプションを提供しており、JSONの出力形式を調整したり、デシリアライズ時の挙動を変更したりできます。これらのオプションは、Json オブジェクトをインスタンス化する際にコンストラクタの引数として指定します。

“`kotlin
import kotlinx.serialization.json.Json

val customJson = Json {
// ここにカスタマイズオプションを記述
prettyPrint = true // 例えば、整形されたJSONを出力する
ignoreUnknownKeys = true // 例えば、JSONに未知のキーがあっても無視する
// … 他のオプション
}
“`

よく使われる、かつ基本的な範囲で重要なオプションをいくつか紹介します。

  1. prettyPrint: Boolean (デフォルト: false)

    • JSON文字列を人間が読みやすいように、インデントや改行を付けて整形して出力します。開発中のデバッグ時などに非常に便利です。
    • true に設定すると、シリアライズ時の出力が見やすくなります。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.encodeToString

    @Serializable
    data class User(val name: String, val age: Int)

    fun main() {
    val user = User(“Alice”, 30)

    // デフォルト設定 (整形されない)
    println("Default: ${Json.encodeToString(user)}")
    // 出力例: Default: {"name":"Alice","age":30}
    
    // prettyPrint = true に設定
    val prettyJson = Json { prettyPrint = true }
    println("Pretty: ${prettyJson.encodeToString(user)}")
    // 出力例:
    // Pretty: {
    //   "name": "Alice",
    //   "age": 30
    // }
    

    }
    “`

  2. ignoreUnknownKeys: Boolean (デフォルト: false)

    • デシリアライズ時に、対応するKotlinクラスのプロパティに存在しないキーがJSON文字列に含まれていた場合、その未知のキーを無視するかどうかを制御します。
    • false の場合(デフォルト)、未知のキーが見つかると UnknownKeyException が発生します。これは、JSONスキーマの変更に気付かずに古いデータ構造でパースしようとした場合などにエラーとして検知できるため、安全性は高いです。
    • true に設定すると、未知のキーは単にスキップされ、エラーになりません。APIからのレスポンスなど、今後フィールドが増える可能性があるが、現在のクライアントでは不要なフィールドを無視したい場合に便利です。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.SerializationException // エラークラス

    @Serializable
    data class User(val name: String, val age: Int)

    fun main() {
    val jsonWithExtraKey = “””{“name”:”Alice”,”age”:30,”city”:”Tokyo”}””” // cityという未知のキーを含む

    // デフォルト設定 (ignoreUnknownKeys = false)
    try {
        Json.decodeFromString<User>(jsonWithExtraKey)
    } catch (e: SerializationException) {
        println("Default (ignoreUnknownKeys=false) caught exception: ${e.message}")
        // 出力例: Default (ignoreUnknownKeys=false) caught exception: Unknown key 'city'
    }
    
    // ignoreUnknownKeys = true に設定
    val lenientJson = Json { ignoreUnknownKeys = true }
    try {
        val user = lenientJson.decodeFromString<User>(jsonWithExtraKey)
        println("With ignoreUnknownKeys=true: Successfully decoded user: ${user.name}, ${user.age}")
        // 出力例: With ignoreUnknownKeys=true: Successfully decoded user: Alice, 30
    } catch (e: SerializationException) {
        println("This should not happen with ignoreUnknownKeys=true")
    }
    

    }
    “`

  3. isLenient: Boolean (デフォルト: false)

    • JSONパース時に、より寛容なモードで解析するかどうかを制御します。
    • false の場合(デフォルト)、JSON標準に厳密に従います。例えば、キーや文字列がダブルクォーテーションで囲まれていない場合、特別なエスケープが必要な文字が含まれている場合などにエラーになります。
    • true に設定すると、キーがクォーテーションされていなかったり、コメントが含まれていたり、特別なエスケープなしで制御文字が使われていたりする、非標準的なJSONでもパースを試みます。設定ファイルなど、手動で編集される可能性のあるJSONを扱う場合に役立つことがありますが、セキュリティリスクを高める可能性もあるため注意が必要です。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.SerializationException

    @Serializable
    data class Item(val name: String)

    fun main() {
    // 非標準的なJSON (キーがクォーテーションされていない)
    val malformedJson = “””{ name: “Laptop” }”””

    // デフォルト設定 (isLenient = false)
    try {
        Json.decodeFromString<Item>(malformedJson)
    } catch (e: SerializationException) {
        println("Default (isLenient=false) caught exception: ${e.message}")
        // 出力例: Default (isLenient=false) caught exception: Unexpected JSON token at offset 2: Expected quotation mark '"', but had 'n' instead
    }
    
    // isLenient = true に設定
    val lenientJson = Json { isLenient = true }
    try {
        val item = lenientJson.decodeFromString<Item>(malformedJson)
        println("With isLenient=true: Successfully decoded item: ${item.name}")
        // 出力例: With isLenient=true: Successfully decoded item: Laptop
    } catch (e: SerializationException) {
        println("This should not happen with isLenient=true")
    }
    

    }
    “`

  4. encodeDefaults: Boolean (デフォルト: false)

    • シリアライズ時に、プロパティの値がデータクラスで定義されたデフォルト値と等しい場合に、そのプロパティをJSON出力に含めるかどうかを制御します。
    • false の場合(デフォルト)、デフォルト値と同じプロパティはJSONから省略されます。これによりJSONがコンパクトになります。
    • true に設定すると、デフォルト値と同じプロパティも含めて、すべてのプロパティがJSONに出力されます。
    • @EncodeDefault アノテーションは、この設定よりも優先されます。@EncodeDefault は特定のプロパティに対して常にエンコードすることを強制し、encodeDefaults = true@EncodeDefault が付いていないすべてのプロパティに対してデフォルト値でもエンコードすることを強制します。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.encodeToString
    import kotlinx.serialization.EncodeDefault

    @Serializable
    data class Config(
    val timeout: Int = 5000, // デフォルト値あり
    @EncodeDefault
    val retries: Int = 3, // デフォルト値あり + EncodeDefault
    val verbose: Boolean = false // デフォルト値あり
    )

    fun main() {
    val defaultConfig = Config() // すべてデフォルト値

    // デフォルト設定 (encodeDefaults = false)
    println("Default Json: ${Json.encodeToString(defaultConfig)}")
    // 出力例: Default Json: {"retries":3} // timeoutとverboseはデフォルト値なので省略、retriesは@EncodeDefaultで出力
    
    // encodeDefaults = true に設定
    val jsonWithDefaults = Json { encodeDefaults = true }
    println("Json with encodeDefaults=true: ${jsonWithDefaults.encodeToString(defaultConfig)}")
    // 出力例: Json with encodeDefaults=true: {"timeout":5000,"retries":3,"verbose":false} // すべて出力される
    

    }
    “`

    通常、encodeDefaults = false (デフォルト) でJSONをコンパクトに保つのが一般的です。特定のプロパティを常に含めたい場合にのみ @EncodeDefault を使用します。

  5. explicitNulls: Boolean (デフォルト: true in 1.3.0+, false before)

    • シリアライズ時に、nullableなプロパティの値が null である場合に、JSON出力に "key": null の形式で明示的に含めるかどうかを制御します。
    • デフォルトは true です(バージョン1.3.0以降)。つまり、nullableなプロパティが null の場合、デフォルトでは "key": null として出力されます。
    • false に設定すると、nullableなプロパティが null の場合、そのキーはJSON出力から省略されます。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.encodeToString

    @Serializable
    data class Contact(val name: String, val email: String?, val phone: String?)

    fun main() {
    val contact = Contact(“Alice”, null, “123-4567”)

    // デフォルト設定 (explicitNulls = true)
    println("Default Json: ${Json.encodeToString(contact)}")
    // 出力例: Default Json: {"name":"Alice","email":null,"phone":"123-4567"} // emailがnullでも明示的に出力
    
    // explicitNulls = false に設定
    val jsonWithoutNulls = Json { explicitNulls = false }
    println("Json with explicitNulls=false: ${jsonWithoutNulls.encodeToString(contact)}")
    // 出力例: Json with explicitNulls=false: {"name":"Alice","phone":"123-4567"} // nullのemailは省略される
    

    }
    “`

    JSON APIの仕様によっては、null値を明示的に出力する必要がある場合("key": null)と、単にキーを省略する必要がある場合(キーなし)があります。このオプションでその挙動を制御できます。デフォルトの true は、デシリアライズ時にキーの存在/非存在とnull値の区別が必要な場合に役立ちますが、JSONが冗長になることもあります。API仕様に合わせて選択してください。

これらのオプションを適切に組み合わせることで、様々なJSON形式に対応したり、開発効率を上げたりすることができます。

@SerialName アノテーション

Kotlinクラスのプロパティ名とJSONのキー名が異なる場合があります。例えば、Kotlinではキャメルケース (userName) を使うことが多いですが、JSONではスネークケース (user_name) が使われることがあります。このような場合に、@SerialName アノテーションを使ってマッピングを定義できます。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

@Serializable
data class UserProfile(
@SerialName(“user_id”) // JSONキーは “user_id” だが、プロパティ名は userId
val userId: Int,
@SerialName(“user_name”) // JSONキーは “user_name” だが、プロパティ名は userName
val userName: String,
val age: Int // このプロパティはKotlin名と同じキー (age) を使用
)

fun main() {
val profile = UserProfile(101, “Alice”, 30)

// シリアライズ: Kotlinのプロパティ名 -> JSONのキー名に変換
val jsonString = Json.encodeToString(profile)
println(jsonString)
// 出力: {"user_id":101,"user_name":"Alice","age":30} // 指定したJSONキー名で出力される

// デシリアライズ: JSONのキー名 -> Kotlinのプロパティ名に変換
val jsonInput = """{"user_id":202,"user_name":"Bob","age":25}"""
val decodedProfile = Json.decodeFromString<UserProfile>(jsonInput)
println("Decoded Profile: userId=${decodedProfile.userId}, userName=${decodedProfile.userName}, age=${decodedProfile.age}")
// 出力: Decoded Profile: userId=202, userName=Bob, age=25 // 指定したJSONキー名からKotlinのプロパティ名にデシリアライズされる

}
“`

@SerialName("...") をプロパティに付与することで、そのプロパティに対応するJSONキーの名前を指定できます。このアノテーションはシリアライズ時とデシリアライズ時の両方に影響します。Kotlin側の名前を変更せずにJSON側の名前だけを変えたい場合に非常に便利です。

@Transient アノテーション

特定のプロパティをシリアライズ/デシリアライズの対象から除外したい場合があります。例えば、一時的な計算結果、セキュリティ上JSONに含めたくない情報、あるいはシリアライズできない型のプロパティなどです。これには @Transient アノテーションを使用します。

@Transient を付与したプロパティは、シリアライズ時には無視され、JSON出力に含まれません。デシリアライズ時には、このプロパティは対象外となるため、データクラスのコンストラクタでデフォルト値が設定されているか、あるいは lateinit や nullable 型など、値が設定されなくてもオブジェクトの構築が可能な方法で定義されている必要があります。デフォルト値を使用するのが最も簡単です。

“`kotlin
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient // Transient はこのパッケージにあります
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

@Serializable
data class UserData(
val id: Int,
val name: String,
@Transient // このプロパティはシリアライズ/デシリアライズから除外
val isAuthenticated: Boolean = false, // @Transient プロパティにはデフォルト値が必要
@Transient
val temporaryToken: String? = null // nullable型でもデフォルト値があればOK
)

fun main() {
val userData = UserData(1, “Alice”, true, “temp-abc”) // isAuthenticatedとtemporaryTokenに値を設定

// シリアライズ: @Transient が付いたプロパティは出力されない
val jsonString = Json.encodeToString(userData)
println(jsonString)
// 出力: {"id":1,"name":"Alice"} // isAuthenticatedとtemporaryTokenは含まれない

// デシリアライズ: @Transient が付いたプロパティはデシリアライズされず、デフォルト値が設定される
val jsonInput = """{"id":2,"name":"Bob"}""" // isAuthenticatedとtemporaryTokenのキーは含まれない
val decodedUserData = Json.decodeFromString<UserData>(jsonInput)
println("Decoded UserData: id=${decodedUserData.id}, name=${decodedUserData.name}, isAuthenticated=${decodedUserData.isAuthenticated}, temporaryToken=${decodedUserData.temporaryToken}")
// 出力: Decoded UserData: id=2, name=Bob, isAuthenticated=false, temporaryToken=null // デフォルト値が設定される

}
“`

@Transient プロパティには必ずデフォルト値を設定してください。そうしないと、デシリアライズ時にそのプロパティに値を設定する方法がなくなり、エラーとなる可能性があります。

これらの高度な設定とアノテーションを使いこなすことで、Kotlin Serializationは様々なJSONフォーマットに対応し、より柔軟で実用的なデータ変換を実現できます。

エラーハンドリング

JSONデータのパースやシリアライズ時には、様々な理由でエラーが発生する可能性があります。例えば、入力されたJSON文字列の形式が不正である、デシリアライズしようとしているKotlinクラスの構造とJSONの構造が一致しない、必須フィールドがJSONに存在しない、値の型が期待と異なるなどです。

Kotlin Serializationは、これらのエラーを検出すると kotlinx.serialization.SerializationException またはそのサブクラスをスローします。適切なエラーハンドリングを行うことで、アプリケーションの安定性を高めることができます。

一般的なエラーとその対処法をいくつか紹介します。

  1. 形式不正なJSON文字列

    • 入力されたJSON文字列の構文が正しいJSON形式としてパースできない場合に発生します。
    • 例: 括弧の対応が取れていない、キーや文字列が適切にクォーテーションされていないなど。
    • 例外: JsonDecodingException (多くの場合、SerializationException のサブクラス)
    • 対処法:
      • try-catch ブロックで例外を捕捉する。
      • isLenient = true オプションで、より寛容なパースを試みる(ただし非推奨の場合あり)。
      • JSON文字列がどこから来るのかを確認し、ソース側で正しいJSON形式を保証する。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.SerializationException

    @Serializable
    data class Item(val name: String)

    fun main() {
    // 不正なJSON形式
    val badJson = “””{“name”: “Laptop”””” // 最後の } が欠けている

    try {
        val item = Json.decodeFromString<Item>(badJson)
        println("Decoded: ${item.name}")
    } catch (e: SerializationException) {
        println("Error decoding JSON: ${e.message}")
        // 出力例: Error decoding JSON: Unexpected end of input at offset 20: Expected closing brace '}'
    }
    

    }
    “`

  2. 必須プロパティの欠落

    • デシリアライズ先のKotlinクラスで非nullableとして定義されているプロパティに対応するキーが、入力JSONに存在しない場合に発生します。
    • 例外: MissingFieldException (多くの場合、SerializationException のサブクラス)
    • 対処法:
      • try-catch ブロックで例外を捕捉する。
      • JSONを提供する側に、必須フィールドを常に含めるように修正を依頼する。
      • Kotlin側のプロパティ定義を見直し、もしそのフィールドが常に必須ではないのであれば、nullable (?) に変更する。
      • プロパティにデフォルト値を設定する。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.SerializationException

    @Serializable
    data class Product(val id: Int, val name: String) // nameは非nullable

    fun main() {
    // name キーが欠落しているJSON
    val jsonWithoutName = “””{“id”: 1}”””

    try {
        val product = Json.decodeFromString<Product>(jsonWithoutName)
        println("Decoded: ${product.id}, ${product.name}")
    } catch (e: SerializationException) {
        println("Error decoding JSON: ${e.message}")
        // 出力例: Error decoding JSON: Field 'name' is required for type with serial name 'Product', but it was missing
    }
    
    // 対処法例:nameをnullableにするかデフォルト値を追加
    @Serializable
    data class ProductFixed(val id: Int, val name: String? = null) // nameをnullableにしてデフォルト値を設定
    
    try {
        val product = Json.decodeFromString<ProductFixed>(jsonWithoutName)
        println("Decoded (fixed): ${product.id}, ${product.name}")
        // 出力: Decoded (fixed): 1, null
    } catch (e: SerializationException) {
         println("This should not happen")
    }
    

    }
    “`

  3. 未知のキー

    • 入力JSONに、デシリアライズ先のKotlinクラスに存在しないキーが含まれている場合に発生します。
    • 例外: UnknownKeyException (デフォルト設定 ignoreUnknownKeys = false の場合)
    • 対処法:
      • try-catch ブロックで例外を捕捉する。
      • ignoreUnknownKeys = true オプションを設定して、未知のキーを無視する。APIレスポンスなど、今後フィールドが増える可能性がある場合に有効です。
      • Kotlinクラスに、未知のキーに対応するプロパティを追加する(ただし、これはJSONスキーマに厳密に従う場合に限られます)。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.SerializationException

    @Serializable
    data class User(val name: String, val age: Int)

    fun main() {
    // address キーが未知のキー
    val jsonWithUnknownKey = “””{“name”:”Alice”,”age”:30,”address”:”Tokyo”}”””

    // デフォルト設定 (ignoreUnknownKeys = false)
    try {
        Json.decodeFromString<User>(jsonWithUnknownKey)
    } catch (e: SerializationException) {
        println("Error decoding JSON: ${e.message}")
        // 出力例: Error decoding JSON: Unknown key 'address'
    }
    
    // ignoreUnknownKeys = true に設定
    val lenientJson = Json { ignoreUnknownKeys = true }
    try {
        val user = lenientJson.decodeFromString<User>(jsonWithUnknownKey)
        println("Decoded with ignoreUnknownKeys=true: ${user.name}, ${user.age}")
        // 出力: Decoded with ignoreUnknownKeys=true: Alice, 30
    } catch (e: SerializationException) {
        println("This should not happen")
    }
    

    }
    “`

  4. 型不一致

    • 入力JSONの値の型が、対応するKotlinプロパティの型と一致しない場合に発生します。例えば、数値型プロパティに対してJSONで文字列が送られてきた場合などです。
    • 例外: JsonDecodingException (多くの場合、SerializationException のサブクラス)
    • 対処法:
      • try-catch ブロックで例外を捕捉する。
      • JSONを提供する側に、正しい型で値を送信するように修正を依頼する。
      • Kotlin側のプロパティ型を見直し、JSONで送られてくる可能性のある型(例: 数値が文字列として送られてくるなら String 型として受け取る)に対応できるように変更し、その後の処理で型変換を行う。

    “`kotlin
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.SerializationException

    @Serializable
    data class Product(val id: Int, val price: Double)

    fun main() {
    // price が文字列として送られているJSON
    val jsonWithTypeMismatch = “””{“id”: 1, “price”: “19.99”}””” // price は Double を期待している

    try {
        val product = Json.decodeFromString<Product>(jsonWithTypeMismatch)
        println("Decoded: ${product.id}, ${product.price}")
    } catch (e: SerializationException) {
        println("Error decoding JSON: ${e.message}")
        // 出力例: Error decoding JSON: Unexpected JSON token at offset 17: Expected FLOAT or DOUBLE, but found STRING instead
    }
    
    // 対処法例:priceをStringとして受け取る
    @Serializable
    data class ProductFixed(val id: Int, val price: String) // priceをStringとして受け取る
    
    try {
        val product = Json.decodeFromString<ProductFixed>(jsonWithTypeMismatch)
        val priceAsDouble = product.price.toDoubleOrNull() // 手動でDoubleに変換
        println("Decoded (fixed): ${product.id}, ${product.price} -> Double: $priceAsDouble")
        // 出力: Decoded (fixed): 1, 19.99 -> Double: 19.99
    } catch (e: SerializationException) {
         println("This should not happen")
    }
    

    }
    “`

デシリアライズ処理は、外部からの入力(JSON文字列)を扱うため、様々な不正なデータが送られてくる可能性があります。これらのエラーケースを想定し、try-catch ブロックを使って例外を捕捉し、適切に処理(ログ出力、デフォルト値の使用、エラーメッセージの表示など)を行うことが、堅牢なアプリケーションを構築する上で非常に重要です。

また、前述の Json オブジェクトのオプション(特に ignoreUnknownKeysisLenient)は、エラー発生を防ぐための設定としても利用できます。ただし、これらのオプションはエラーを「無視」する可能性があるため、本来検出されるべき問題を見逃さないよう、慎重に使用する必要があります。基本的には、厳密なパース設定 (ignoreUnknownKeys = false, isLenient = false) でエラーを検出し、必要に応じて例外ハンドリングやJSON/Kotlinクラス定義の見直しで対応するのがおすすめです。

Kotlin Serializationの利点

ここまでKotlin Serializationの基本的な使い方とカスタマイズ、エラーハンドリングを見てきました。改めて、Kotlin Serializationが提供する主な利点をまとめます。

  1. Kotlinネイティブな設計:

    • データクラス、null安全性、コルーチンなど、Kotlin言語の特性を活かした設計になっています。これにより、Kotlinらしい簡潔で安全なコードでJSON変換を記述できます。特にnull安全性のサポートは、デシリアライズ時のエラーを減らす上で強力です。
    • コンパイル時のコード生成により、リフレクションを使用しません。これにより、Kotlin/Nativeのようなリフレクションが制限される環境でも動作し、マルチプラットフォーム開発に適しています。
  2. コンパイル時の安全性:

    • @Serializable アノテーションが付与されたクラスは、コンパイル時にシリアライズ可能かどうかがチェックされます。サポートされていない型を使っている場合や、@Transient にデフォルト値がない場合など、多くの問題をコンパイル段階で検出できるため、実行時エラーを未然に防ぎやすくなります。
  3. マルチプラットフォーム対応:

    • JVM、JS、Nativeの各プラットフォームで共通のシリアライズロジックを使用できます。これにより、共通コードでデータモデルを定義し、異なるプラットフォーム間でデータをやり取りする処理を効率的に実装できます。
  4. パフォーマンス:

    • リフレクションを使用せず、コンパイル時に生成された専用コードを使うため、一般的にJacksonやGsonなどのリフレクションベースのライブラリよりも高速なシリアライズ・デシリアライズ処理が期待できます。
  5. 柔軟なカスタマイズ性:

    • Json オブジェクトの豊富なオプションにより、JSONの出力形式やデシリアライズ時の挙動を細かく制御できます。
    • @SerialName@Transient などのアノテーションにより、Kotlinのプロパティ名とJSONキー名のマッピングや、プロパティの除外を柔軟に行えます。
    • カスタムシリアライザー/デシリアライザーを記述することで、標準でサポートされていない型(例: java.time.LocalDateTime など)や、特殊な形式のデータも扱うことができます(この記事では詳細を割愛しましたが、高度な機能として提供されています)。
  6. 統一されたAPI:

    • JSONだけでなく、CBOR、Protocol Buffers、Propertiesなどの様々なデータフォーマットに対して、共通のシリアライズ/デシリアライズAPI (encodeToString, decodeFromString など) を使用できます。これにより、フォーマットが変わっても学習コストやコードの変更量を抑えることができます。

これらの利点から、Kotlin Serializationは特にKotlinプロジェクトにおいて、JSON変換ライブラリとして非常に魅力的な選択肢となります。

他のライブラリとの比較 (Jackson, Gsonなど)

Kotlin Serialization以外にも、Javaの世界で広く使われているJSONライブラリ(Jackson, Gson, Moshiなど)をKotlinプロジェクトで使用することは可能です。これらのライブラリもKotlin対応を進めていますが、Kotlin Serializationと比較した場合の主な違いを理解しておくことは、どのライブラリを選択するかを判断する上で役立ちます。

特徴 Kotlin Serialization Jackson / Gson / Moshi (Kotlin対応)
基盤技術 コンパイル時のコード生成、リフレクション不使用 実行時のリフレクション (一部ライブラリはコード生成もサポート)
Kotlin対応 Kotlinネイティブ、Null安全性・データクラスに最適化 Kotlinモジュール等で対応、Null安全性やImmutableクラスの扱いに制約がある場合がある (ライブラリによる)
コンパイル時安全性 @Serializable による強力なチェック、エラー早期検出 実行時エラーが多い
マルチプラットフォーム JVM, JS, Native 対応 (共通コードで利用可能) 主にJVM向け (JS, Native対応は限定的または非公式)
パフォーマンス 一般的に高速 リフレクションのオーバーヘッドがあるが、十分に高速な場合が多い (特にGsonは高速とされる)
設定の容易さ シンプルなケースは容易、カスタマイズはJson設定/アノテーション アノテーション主体、設定オプションも豊富だがライブラリ固有の記述が必要
フォーマット対応 JSON, CBOR, ProtoBuf, Properties など (公式) JSONがメイン、XMLや他のフォーマットも扱うモジュールがある (ただしAPIはJSONと異なる場合が多い)
エコシステム 新しいが成長中、Kotlin中心 歴史があり成熟、Javaエコシステムに深く統合されている、多くの拡張やデータソース対応がある (DB連携など)
学習コスト Kotlin開発者には馴染みやすいコンセプト、新しいAPI Java開発者には馴染みやすい、Kotlin特有の注意点がある

Kotlin Serializationを選ぶべきケース:

  • 新しいKotlinプロジェクトを開始する場合。
  • マルチプラットフォーム開発を行っており、異なるプラットフォーム間でデータモデルを共有したい場合。
  • Null安全性やImmutableなデータクラスを厳密に扱いたい場合。
  • コンパイル時の安全性を重視し、実行時エラーを減らしたい場合。
  • JSON以外のフォーマット(CBOR, Protocol Buffersなど)も将来的に扱う可能性がある場合。

既存のJackson/Gsonプロジェクトで使い続けるケース:

  • 既に大規模なJackson/Gsonベースのコードがあり、移行コストが大きい場合。
  • JacksonやGsonが提供する特定の高度な機能(例: 特定のデータソースへの直接連携、複雑なカスタムシリアライザーなど)に依存している場合。
  • Javaコードとの相互運用性が非常に重要で、共通のライブラリを使用したい場合。
  • エコシステムの成熟度や、特定のフレームワークとの連携を重視する場合。

最近のJacksonやGsonはKotlinモジュールや拡張を提供しており、以前に比べてKotlinとの親和性は向上しています。しかし、Kotlin Serializationは最初からKotlinのために設計されているため、Kotlin言語のメリットを最大限に引き出せる点が大きな強みです。

特にこれからKotlinで新規プロジェクトを始める場合や、既存のプロジェクトでもJSON変換部分をクリーンに保ちたい場合は、Kotlin Serializationを積極的に検討する価値は十分にあります。

まとめ

この記事では、Kotlin Serializationライブラリを用いたJSONデータ変換の基本的な方法について、プロジェクトへの導入から、シリアライズ可能なクラスの定義、実際の変換処理、そして便利なカスタマイズオプションやエラーハンドリングまで、詳細に解説しました。

学んだ主要なポイントは以下の通りです。

  • 導入: Gradleに kotlin-serialization プラグインと kotlinx-serialization-json 依存関係を追加します。
  • シリアライズ可能クラス: @Serializable アノテーションをデータクラスなどに付与することで、シリアライズ/デシリアライズの対象とします。組み込み型、コレクション、入れ子構造、Nullableプロパティ、デフォルト値などをサポートします。
  • 変換: Json オブジェクトの encodeToString() メソッドでKotlinオブジェクトをJSONに、decodeFromString() メソッドでJSONをKotlinオブジェクトに変換します。
  • カスタマイズ: Json { ... } ブロックで prettyPrint, ignoreUnknownKeys, isLenient, encodeDefaults, explicitNulls などのオプションを設定し、変換の挙動を調整できます。
  • アノテーション: @SerialName でJSONキー名とプロパティ名をマッピングし、@Transient でプロパティを変換対象から除外できます。
  • エラーハンドリング: JSON形式の不正、必須フィールドの欠落、未知のキー、型不一致などにより SerializationException が発生する可能性があるため、try-catch によるハンドリングや、適切な設定オプションの活用が必要です。
  • 利点: Kotlinとの高い親和性、コンパイル時の安全性、マルチプラットフォーム対応、パフォーマンス、柔軟なカスタマイズ性などが挙げられます。

Kotlin Serializationは、KotlinプロジェクトにおけるJSON変換のベストプラクティスの一つと言えます。コンパイル時の安全性とKotlinネイティブな設計は、開発効率とアプリケーションの堅牢性を向上させます。

この記事で紹介した内容は、Kotlin Serializationを使ったJSON変換の「基本」です。ライブラリには、カスタムシリアライザー、ポリモーフィックシリアライズ(継承・多態性の扱い)、ストリームベースの変換など、さらに高度な機能も多数用意されています。これらの機能は、より複雑な要件に対応する必要がある場合に役立ちます。

まずはこの記事で解説した基本をしっかりと理解し、日々の開発で活用してみてください。JSONデータの扱いは、現代のアプリケーション開発において避けて通れない課題であり、Kotlin Serializationはその課題を効率的かつ安全に解決するための強力なツールとなるでしょう。

Happy coding!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール