KotlinのSerializationの使い方について

採用はこちら

KotlinでJSONを扱うときによく使われるのが、kotlinx.serialization です。

これは、KotlinオブジェクトをJSON文字列に変換したり、JSON文字列をKotlinオブジェクトに戻したりするための公式ライブラリです。

Kotlin Serializationの大きな特徴は、@Serializable を付けたクラスに対して、コンパイラがシリアライズ用のコードを自動生成してくれることです。

そのため、Kotlinの型システムと相性がよく、型安全に扱いやすいのが強みです。

さらに、Kotlin Multiplatformにも対応しており、JSON以外にProtobuf、CBOR、HOCON、Propertiesなどの形式も扱えます。

ただし、実務で最もよく使われるのはJSONです。まずはJSONを中心に理解するのが分かりやすいです。

目次

Kotlin Serializationでできること

Kotlin Serializationでは、主に次のような処理ができます。

  • KotlinのデータクラスをJSONに変換する
  • JSON文字列をKotlinのデータクラスに変換する
  • ネストしたオブジェクトやリストをそのまま扱う
  • JSONキー名とKotlinプロパティ名が違う場合に対応する
  • APIレスポンスの余分なキーを無視する
  • sealed class を使った多態的なデータ構造を扱う
  • 必要に応じて独自のシリアライズ処理を定義する

つまり、単純なJSONの読み書きだけでなく、実際のAPI通信で必要になることの多くをカバーできます。

まずはセットアップ

Kotlin Serializationを使うには、Gradleのプラグイン依存関係を追加する必要があります。

build.gradle.kts の例

plugins {
    kotlin("jvm") version "2.3.20"
    kotlin("plugin.serialization") version "2.3.20"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
}

ここで重要なのは次の2つです。

  • kotlin("plugin.serialization") を追加すること
  • JSONを扱うために kotlinx-serialization-json を追加すること

この2つがそろっていないと、@Serializable を付けても正しく動かないことがあります。

最小構成の使い方

最初に覚えるべき基本はとてもシンプルです。

クラスに @Serializable を付ける

import kotlinx.serialization.Serializable

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

KotlinオブジェクトをJSONに変換する

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

fun main() {
    val user = User(1, "Taro")
    val json = Json.encodeToString(user)
    println(json)
}

出力例

{"id":1,"name":"Taro"}

JSONからKotlinオブジェクトに戻す

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

fun main() {
    val json = """{"id":1,"name":"Taro"}"""
    val user = Json.decodeFromString<User>(json)
    println(user)
}

出力例

User(id=1, name=Taro)

この流れがKotlin Serializationの基本です。

実務では Json { ... } を設定して使う

実際の開発では、Json をそのまま使うより、設定を加えて使うことが多いです。

val json = Json {
    prettyPrint = true
    ignoreUnknownKeys = true
    encodeDefaults = false
    explicitNulls = true
}

この設定はよく使います。

prettyPrint = true

JSONを整形して見やすく出力します。

ログ確認やデバッグ時に便利です。

ignoreUnknownKeys = true

JSON側に、Kotlinのクラスで定義していない項目が含まれていても無視します。

外部APIと連携するときに非常に重要です。

encodeDefaults = false

デフォルト値と同じ値を持つプロパティを、JSONに出力しない設定です。

これがデフォルトの挙動です。

explicitNulls = true

null を明示的にJSONへ出力する設定です。

これもデフォルトでは true です。

デフォルト値の扱い

Kotlin Serializationでは、プロパティがデフォルト値と同じ場合、JSON出力から省略されることがあります

@Serializable
data class Profile(
    val name: String,
    val age: Int = 20
)
val profile = Profile("Taro")
println(Json.encodeToString(profile))

この場合、age20 なので、省略される可能性があります。

出力例

{"name":"Taro"}

すべての値を必ず出力したい場合は、encodeDefaults = true を使います。

val json = Json {
    encodeDefaults = true
}

これでデフォルト値も含めて出力されます。

null の扱い

nullableな値は普通に扱えます。

@Serializable
data class Product(
    val name: String,
    val description: String? = null
)

ただし、null をJSONにどう出力するかは explicitNulls の設定に影響されます。

explicitNulls = true

null を明示的に出力します。

{"name":"Book","description":null}

explicitNulls = false

null の項目そのものを省略します。

{"name":"Book"}

APIによっては、null を送るのか、項目ごと省略するのかで意味が変わることがあります。

この違いはかなり重要です。

JSONキー名が違う場合は @SerialName

APIのJSONキー名と、Kotlinのプロパティ名が一致しないことはよくあります。

その場合は @SerialName を使います。

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Article(
    @SerialName("article_id")
    val id: Int,

    @SerialName("article_title")
    val title: String
)

このように書くと、JSONの article_id がKotlinの id に対応し、article_titletitle に対応します。

これはAPI連携では非常によく使う機能です。

一括で命名規則を変えたい場合

個別に @SerialName を付けるだけでなく、全体の命名規則をまとめて変換したいケースもあります。

たとえば、Kotlin側では camelCase、JSON側では snake_case を使いたい場合です。

このようなケースでは namingStrategy を使う方法もあります。

ただし、入門段階ではまず @SerialName を覚えるほうが分かりやすいです。

考え方としては次のようになります。

  • 個別に対応したいなら @SerialName
  • 全体の規則を統一したいなら namingStrategy

リストやMapもそのまま扱える

Kotlin Serializationは、データクラスだけでなく、ListやMapのような標準コレクションも扱えます。

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

fun main() {
    val users = listOf(
        User(1, "Taro"),
        User(2, "Hanako")
    )

    val json = Json.encodeToString(users)
    println(json)

    val decoded = Json.decodeFromString<List<User>>(json)
    println(decoded)
}

JSON配列とKotlinの List をそのまま相互変換できるので、APIレスポンスを扱いやすいです。

ネストしたオブジェクトも扱える

データクラスの中に別のデータクラスがある構造も普通に扱えます。

@Serializable
data class Address(
    val city: String,
    val zipCode: String
)

@Serializable
data class Customer(
    val name: String,
    val address: Address
)
val customer = Customer(
    "Taro",
    Address("Kyoto", "600-0000")
)

val json = Json.encodeToString(customer)
println(json)

このように、クラスの中に別の @Serializable クラスが入っていても問題ありません。

APIレスポンスでは ignoreUnknownKeys が特に重要

外部APIを使うと、サーバー側の仕様変更でJSONに新しいフィールドが増えることがあります。

たとえば、次のようなJSONが返ってきたとします。

{"id":1,"name":"Taro","extra":"value"}

Kotlin側のクラスがこうだった場合:

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

そのままだと、extra を知らないためエラーになることがあります。

これを防ぐのが ignoreUnknownKeys = true です。

val json = Json {
    ignoreUnknownKeys = true
}

APIクライアントを書くときは、かなりの頻度で使う設定です。

JsonElement で柔軟に扱う方法

JSONの構造が固定されていないときや、一部だけ動的に処理したいときは JsonElement が便利です。

import kotlinx.serialization.json.*

fun main() {
    val element = Json.parseToJsonElement(
        """{"name":"Taro","age":30}"""
    )

    val obj = element.jsonObject
    println(obj["name"]?.jsonPrimitive?.content)
}

JsonElement を使うと、いったんJSONをツリー構造として受け取り、必要な部分だけ手動で見られます。

これは次のような場面で役立ちます。

  • APIレスポンスの構造が不安定
  • JSONの一部だけ見たい
  • 条件によって読み方を変えたい
  • いきなり型付きクラスに落とし込めない

カスタムシリアライザ

標準機能だけで十分なことも多いですが、特殊な形式を扱いたいときはカスタムシリアライザを作れます。

たとえば、独自のID型をJSONでは文字列として扱いたい場合です。

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

object UserIdSerializer : KSerializer<UserId> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("UserId", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: UserId) {
        encoder.encodeString(value.value)
    }

    override fun deserialize(decoder: Decoder): UserId {
        return UserId(decoder.decodeString())
    }
}

@Serializable(with = UserIdSerializer::class)
data class UserId(val value: String)

@Serializable
data class User(
    val id: UserId,
    val name: String
)

このようにしておくと、Kotlin側では UserId という独自型で安全に扱いながら、JSON上ではただの文字列として扱えます。

sealed class と polymorphism

少し進んだ話になりますが、sealed class を使った多態的なデータ構造も扱えます。

import kotlinx.serialization.Serializable

@Serializable
sealed class Payment

@Serializable
data class CreditCard(
    val number: String
) : Payment()

@Serializable
data class BankTransfer(
    val account: String
) : Payment()

このような場合、JSONに変換するときには「どのサブクラスなのか」を識別するための情報が必要になります。

この識別用キーが classDiscriminator です。

val json = Json {
    classDiscriminator = "_type"
}

デフォルトでは "type" が使われますが、API仕様によっては別名に変更したいことがあります。

このあたりは入門者には少し難しい部分ですが、要点は単純です。

  • 普通のデータクラスなら特に意識しなくてよい
  • sealed class や interface をJSON化するときは型情報が必要になる
  • その型情報のキー名は classDiscriminator で調整できる

特殊な浮動小数点値

通常のJSONでは、NaNInfinity はそのまま扱えません。

ただし、allowSpecialFloatingPointValues = true を有効にすると扱えるようになります。

val json = Json {
    allowSpecialFloatingPointValues = true
}

ただし、これは相手側システムも同じ表現を理解できる場合に限って使うのが安全です。

外部APIに送る用途では注意が必要です。

実務でよくある定番の書き方

APIレスポンス用のDTOを作るなら、次のような形がよく使われます。

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
data class UserResponse(
    @SerialName("user_id")
    val id: Int,
    val name: String,
    val email: String? = null
)

fun main() {
    val json = Json {
        ignoreUnknownKeys = true
        prettyPrint = true
        explicitNulls = true
    }

    val raw = """
        {
          "user_id": 1,
          "name": "Taro",
          "email": "taro@example.com",
          "extra_field": "ignored"
        }
    """.trimIndent()

    val user = json.decodeFromString<UserResponse>(raw)
    println(user)

    val encoded = json.encodeToString(user)
    println(encoded)
}

この形なら、API連携でよく必要になるポイントを一通り押さえられます。

  • JSONキー名の違いに対応できる
  • 余分なキーを無視できる
  • nullable項目を扱える
  • KotlinオブジェクトとJSONを往復できる

よくあるエラー

Serializer for class ... is not found

かなりよく出るエラーです。

主な原因は次の通りです。

  • @Serializable を付け忘れている
  • kotlin("plugin.serialization") を設定していない
  • 独自型に対するSerializerが必要なのに未設定
  • Kotlin本体とライブラリのバージョンの組み合わせが不適切

まずは、クラスに @Serializable が付いているか、Gradle設定が正しいかを確認してください。

余分なキーで落ちる

ignoreUnknownKeys = true を設定していない可能性があります。

null の出力が想定と違う

explicitNulls の設定を確認します。

デフォルト値が出力されない

encodeDefaults = true が必要な可能性があります。

type が衝突する

sealed class 周りで classDiscriminator の名前変更が必要なことがあります。

最初に覚えるべきポイント

最初は、次の内容だけ押さえれば十分です。

  1. クラスに @Serializable を付ける
  2. GradleにプラグインとJSON依存関係を追加する
  3. Json.encodeToString() でJSON化する
  4. Json.decodeFromString() で復元する
  5. API用途なら ignoreUnknownKeys = true を覚える
  6. JSONキー名が違うときは @SerialName を使う
  7. null とデフォルト値の扱いとして explicitNullsencodeDefaults を理解する

このあたりまで理解できれば、かなり実用レベルです。

まとめ

Kotlin Serializationは、KotlinでJSONを安全かつ自然に扱うための標準的な方法です。

とくにデータクラスとの相性がよく、API通信や設定ファイルの読み書きなど、さまざまな場面で使えます。

まずは次の流れを覚えるのがおすすめです。

  • @Serializable を付ける
  • Json.encodeToString() でJSONにする
  • Json.decodeFromString() で戻す
  • 必要に応じて ignoreUnknownKeys@SerialNameexplicitNullsencodeDefaults を使う

ここまで理解できれば、日常的なJSON処理ではかなり困らなくなります。

以上、KotlinのSerializationの使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次