#はじめに
Kotlin Serialization ガイドの目次に基づいて「Kotlin Serialization guide」を訳しています。
なお、翻訳にはDeepLの力を99%借りています。
もし、一緒に「Kotlin Serialization ガイド」の翻訳をして下さる方がいらっしゃいましたら、コメント欄などから連絡をください。
##第1章 基本的なシリアライゼーション<原文>
Latest commit 728e220 on 24 Nov 2020版
Kotlin Serialization Guideの第1章です。この章では、Kotlin Serializationの基本的な使い方を示し、その核となる概念を解説しています。
目次
#基本
オブジェクトツリーを文字列またはバイト列に変換するには、相互に絡み合う 2 つのプロセスを経る必要があります。最初のステップでは、オブジェクトはシリアライズ(直列化)され、その構成する直列化したプリミティブ値の変換されます。このプロセス(過程)はすべてのデータ形式に共通しており、その結果はシリアル化されるオブジェクトに依存します。シリアライザは、このプロセスを制御します。2 番目のステップはエンコーディングと呼ばれ、対応する一連のプリミティブな値を出力フォーマットで定めた表現に変換します。エンコーダがこのプロセスを制御します。この区別が重要でない場合は、エンコーディングとシリアライゼーションの両方の用語を互換性を持って使用します。
+---------+ Serialization +------------+ Encoding +---------------+
| Objects | --------------> | Primitives | ---------> | Output format |
+---------+ +------------+ +---------------+
逆の処理は、入力フォーマットの解析とプリミティブ値ので案コード(復元)から始まり、結果として得られたストリームをオブジェクトに_デシリアライズすることになります。この処理の詳細については後ほど説明します。
先ずは、JSON のエンコーディングから始めます。
JSON エンコーディング
データを特定の形式に変換するプロセス全体を“エンコーディング”と呼びます。JSONの場合は、Json.encodeToString拡張関数を使ってデータをエンコードします。これは、パラメータとして渡されたオブジェクトをシリアル化し、JSON 文字列にエンコードします。
プロジェクトを記述するクラスから始めて、そのJSON表現を取得してみましょう。
class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(data))
}
完全なコードはこちら.から取得できます。
このコードを実行すると、例外が発生します。
スレッド "main" での例外 kotlinx.serialization.SerializationException. クラス 'Project' 用のシリアライザが見つかりません。
クラスを @Serializable としてマークするか、シリアライザを明示的に提供します。
シリアライズ可能なクラスは明示的にマークする必要があります。Kotlin Serialization はリフレクションを使用しないので、シリアル化できるはずのないクラスを誤ってデシリアライズしてしまうことはありません。これを修正するために、@Serializable
アノテーションを追加しました。
@Serializable
class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
Serializable` アノテーションは、 Kotlin Serialization プラグインにこのクラス用のシリアライザを自動的に生成して、フックアップするように指示します。これで、例の出力は対応するJSONになりました。
{"name":"kotlinx.serialization","language":"Kotlin"}
Serializersについての章があります。今のところ、Kotlin Serializationプラグインによって自動的に生成されることを知っていれば十分です。
JSON デコーディング
この逆の処理をデコーディングと呼びます。JSON 文字列をオブジェクトにデコードするには、Json.decodeFromString拡張関数を使用します。結果として取得したい型を指定するために、この関数に型パラメータを指定します。
この後示すように、シリアライズは異なる種類のクラスで動作します。ここでは Project
クラスを データクラス
としてマークしているが、これは必須ではなく、その内容を印刷してデコード方法を確認したいからである。
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization","language":"Kotlin"}
""")
println(data)
}
完全なコードはこちらから取得できます。
このコードを実行すると、オブジェクトが戻ってきます。
Project(name=kotlinx.serialization, language=Kotlin)
シリアライゼーション クラス
このセクションでは、異なる @Serializable
クラスがどのように扱われるかについて詳しく説明します。
バッキングフィールドはシリアル化されています。
バッキングフィールドを持つクラスのプロパティのみがシリアライズされるため、以下の例のようにバッキングフィールドを持たないゲッター/セッターとデリゲートされたプロパティはシリアライズされません。
@Serializable
class Project(
// name is a property with backing field -- serialized
var name: String
) {
var stars: Int = 0 // property with a backing field -- serialized
val path: String // getter only, no backing field -- not serialized
get() = "kotlin/$name"
var id by ::name // delegated property -- not serialized
}
fun main() {
val data = Project("kotlinx.serialization").apply { stars = 9000 }
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
JSON出力には name
と stars
プロパティだけが存在することがわかります。
{"name":"kotlinx.serialization","stars":9000}
コンストラクタのプロパティ要件
プロジェクトクラスを定義してパス文字列を受け取り、それを対応するプロパティに分解するようにしたい場合、以下のコードのようなものを書きたくなるかもしれません。
@Serializable
class Project(path: String) {
val owner: String = path.substringBefore('/')
val name: String = path.substringAfter('/')
}
Serializable アノテーションでは、クラスのプライマリ・コンストラクタのすべてのパラメータをプロパティにする必要があるため、このクラスはコンパイルできません。簡単な回避策は、クラスのプロパティを持つプライベートなプライマリ・コンストラクタを定義し、必要なコンストラクタをセカンダリ・コンストラクタに変更することです。
@Serializable
class Project private constructor(val owner: String, val name: String) {
constructor(path: String) : this(
owner = path.substringBefore('/'),
name = path.substringAfter('/')
)
val path: String
get() = "$owner/$name"
}
シリアライズはプライベートプライマリコンストラクタで動作し、まだバッキングフィールドのみをシリアライズしています。
fun main() {
println(Json.encodeToString(Project("kotlin/kotlinx.serialization")))
}
完全なコードはこちらから取得できます。
この例では、期待される出力が得られます。
{"owner":"kotlin","name":"kotlinx.serialization"}
データの検証
プロパティを使用せずにプライマリ コンストラクタのパラメータを導入したい場合は、プロパティに格納する前にその値を検証したい場合もあります。シリアライズができるようにするには、プライマリ・コンストラクタのパラメータをプロパティに置き換え、バリデーションを init { ... }
ブロックに移動します。
@Serializable
class Project(val name: String) {
init {
require(name.isNotEmpty()) { "name cannot be empty" }
}
}
デシリアライズ処理はKotlinの通常のコンストラクタのように動作し、すべての init
ブロックを呼び出し、デシリアライズの結果として無効なクラスを取得できないようにします。試してみましょう。
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":""}
""")
println(data)
}
完全なコードはこちらから取得できます。
このコードを実行すると例外が発生します。
Exception in thread "main" java.lang.IllegalArgumentException: name cannot be empty
オプションのプロパティ
オブジェクトは、そのプロパティがすべて入力に存在する場合にのみデシリアライズすることができます。例えば、以下のコードを実行します。
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization"}
""")
println(data)
}
敢然なコードはこちらから取得することができます。
例外を発生させます。
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required, but it was missing
この問題は、プロパティにデフォルト値を追加することで修正することができます。
@Serializable
data class Project(val name: String, val language: String = "Kotlin")
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization"}
""")
println(data)
}
完全なコードはこちらから取得できます。
これは、language
プロパティのデフォルト値を指定して以下の出力を生成します。
Project(name=kotlinx.serialization, language=Kotlin)
オプションのプロパティイニシャライザの呼び出し
入力にオプションのプロパティが存在する場合、このプロパティに対応するイニシャライザは呼び出されません。これはパフォーマンスのために設計された機能なので、イニシャライザの副作用に頼らないように注意してください。以下の例を考えてみましょう。
fun computeLanguage(): String {
println("Computing")
return "Kotlin"
}
@Serializable
data class Project(val name: String, val language: String = computeLanguage())
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization","language":"Kotlin"}
""")
println(data)
}
完全なコードはこちらから取得することができます。
入力に language
プロパティが指定されているので、出力には "Computing" という文字列は表示されません。
Project(name=kotlinx.serialization, language=Kotlin)
必要なプロパティ
デフォルト値を持つプロパティは、@Required
アノテーションを使ってシリアル形式で要求することができます。先ほどの例を変更して、language
プロパティに @Required
を指定してみましょう。
@Serializable
data class Project(val name: String, @Required val language: String = "Kotlin")
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization"}
""")
println(data)
}
完全なコードはこちらから取得できます。
以下のような例外が発生します。
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required, but it was missing
トランジェントプロパティ
プロパティを @Transient
アノテーションでマークすることで、シリアライズから除外することができます (これを kotlin.jvm.Transient と混同しないようにしてください)。Transientプロパティはデフォルト値を持つ必要があります。
@Serializable
data class Project(val name: String, @Transient val language: String = "Kotlin")
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization","language":"Kotlin"}
""")
println(data)
}
フルコードこちらから取得することができます。
指定された値がデフォルト値と同じであっても、シリアル形式で明示的に値を指定しようとすると、以下のような例外が発生します。
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 60: Encountered an unknown key 'language'.
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
「ignoreUnknownKeys」の機能については、Ignoring Unknown Keysの項で説明しています。
デフォルト値はエンコードされない
JSONではデフォルト値は、あらかじめ設定された状態ではエンコードされません。この動作は、ほとんどの架空ではないシナリオにおいて、このような設定が視覚的な乱雑さを減らし、シリアライズされるデータ量を節約するという事実に基づいています。
@Serializable
data class Project(val name: String, val language: String = "Kotlin")
fun main() {
val data = Project("kotlinx.serialization")
println(Json.encodeToString(data))
}
完全なコードはこちらから取得することができます。
これは以下の出力を生成しますが、その値はデフォルトのものと同じであるため、language
プロパティを持ちません。
{"name":"kotlinx.serialization"}
この動作がJSONに対してどのように設定できるかについては、Encoding defaultsのセクションを参照してください。
ヌル可能なプロパティ
ヌル可能なプロパティは Kotlin Serialization でネイティブにサポートされています。
@Serializable
class Project(val name: String, val renamedTo: String? = null)
fun main() {
val data = Project("kotlinx.serialization")
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
この例では、デフォルト値がエンコードされていないはで、JSONに null
をエンコードしません。
{"name":"kotlinx.serialization"}
型安全性の確保
Kotlin SerializationはKotlinプログラミング言語の型の安全性を強く強制します。具体的には、JSONオブジェクトから null
の値を、 NULL ではない Kotlin のプロパティ language
にデコードしてみましょう。
@Serializable
data class Project(val name: String, val language: String = "Kotlin")
fun main() {
val data = Json.decodeFromString<Project>("""
{"name":"kotlinx.serialization","language":null}
""")
println(data)
}
完全なコードはこちらから取得できます。
language プロパティがデフォルト値を持っているにもかかわらず、null値を代入しようとするとエラーになります。
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found.
Use 'coerceInputValues = true' in 'Json {}` builder to coerce nulls to default values.
サードパーティのJSONをデコードする際に、
null
をデフォルト値に強制することが望まれるかもしれません。これに対応する機能は、入力値の強制のセクションで説明しています。
参照されたオブジェクト
シリアライズ可能なクラスは、そのシリアライズ可能なプロパティで他のクラスを参照することができます。参照されるクラスは @Serializable
としてもマークされていなければなりません。
@Serializable
class Project(val name: String, val owner: User)
@Serializable
class User(val name: String)
fun main() {
val owner = User("kotlin")
val data = Project("kotlinx.serialization", owner)
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
JSONにエンコードされると、ネストされたJSONオブジェクトになります。
{"name":"kotlinx.serialization","owner":{"name":"kotlin"}}
シリアライズ不可能なクラスへの参照は、Transient propertiesとしてマークするか、Serializersの章で示されているように、カスタムシリアライザを提供することができます。
繰り返し参照を圧縮しないでください。
Kotlin Serializationはプレーンデータのエンコードとデコードのために設計されています。オブジェクト参照が繰り返される任意のオブジェクトグラフの再構成はサポートしていません。例えば、同じ owner
インスタンスを二度参照しているオブジェクトをシリアライズしてみましょう。
@Serializable
class Project(val name: String, val owner: User, val maintainer: User)
@Serializable
class User(val name: String)
fun main() {
val owner = User("kotlin")
val data = Project("kotlinx.serialization", owner, owner)
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
単純に2回エンコードされた owner
値を取得するだけである。
{"name":"kotlinx.serialization","owner":{"name":"kotlin"},"maintainer":{"name":"kotlin"}}
円形構造体をシリアライズしようとすると、スタックオーバーフローになります。 トランジェントプロパティを使用して、一部の参照をシリアライズから除外することができます。
ジェネリッククラス
Kotlin の汎用クラスは型ポリモーフィックな振る舞いを提供し、コンパイル時に Kotlin Serialization によって強制されます。例えば、一般的なシリアライズ可能なクラス Box<T>
を考えてみましょう。
@Serializable
class Box<T>(val contents: T)
Box<T>
クラスは Int
のような組み込み型や Project
のようなユーザ定義型と一緒に使うことができます。
@Serializable
class Data(
val a: Box<Int>,
val b: Box<Project>
)
fun main() {
val data = Data(Box(42), Box(Project("kotlinx.serialization", "Kotlin")))
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
JSONで取得する実際の型は、Box
で指定されたコンパイル時の型パラメータに依存します。
{"a":{"contents":42},"b":{"contents":{"name":"kotlinx.serialization","language":"Kotlin"}}}
実際の汎用型がシリアライズできない場合、コンパイル時のエラーが発生します。
シリアルフィールド名
エンコードされたepresentation、例ではJSONで使用されるプロパティの名前は、デフォルトではソースコードでの名前と同じです。シリアライズに使われる名前は serial name と呼ばれ、@SerialName
アノテーションを使って変更することができます。例えば、language
プロパティに省略されたシリアル名を指定することができます。
@Serializable
class Project(val name: String, @SerialName("lang") val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(data))
}
完全なコードはこちらから取得できます。
これで、JSON出力に lang
という略称が使われていることがわかりました。
{"name":"kotlinx.serialization","lang":"Kotlin"}
次の章では、ビルトインクラスについて説明します。