#はじめに
Kotlin Serialization ガイドの目次に基づいて「Kotlin Serialization guide」を訳しています。
なお、翻訳にはDeepLの力を99%借りています。
もし、一緒に「Kotlin Serialization ガイド」の翻訳をして下さる方がいらっしゃいましたら、コメント欄などから連絡をください。
第4章 ポリモフィズム<原文>
Latest commit 728e220 on 24 Nov 2020版
Kotlinシリアライゼーションガイドの第4章です。
この章では、Kotlin Serialization がどのように多態性クラスの階層を扱うかを見ていきます。
Table of contents
閉じたポリモフィズム
まずはポリモーフィズムの基本的な紹介から始めてみましょう。
静的な型
Kotlin のシリアライズは default によって型に関して完全に静的なものになります。エンコードされたオブジェクトの構造は オブジェクトの compile-time 型によるものです。この側面をより詳しく調べて、どのように を使って多態データ構造をシリアライズします。
Kotlin のシリアライズの静的な性質を示すために、以下のような設定をしてみましょう。open class Project
にはname
のプロパティがあるだけで、その派生class OwnedProject
にはowner
のプロパティが追加されています。以下の例では、実行時にOwnedProject
のインスタンスで初期化されるProject
の静的型でdata
変数をシリアライズしています。
@Serializable
open class Project(val name: String)
class OwnedProject(name: String, val owner: String) : Project(name)
fun main() {
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(Json.encodeToString(data))
}
フルコードはこちらから取得できます。
OwnedProject
のランタイムタイプにもかかわらず、Project
クラスのプロパティだけがシリアライズされてしまいます。
{"name":"kotlinx.coroutines"}
コンパイル時の data
の型を OwnedProject
に変更してみましょう。
@Serializable
open class Project(val name: String)
class OwnedProject(name: String, val owner: String) : Project(name)
fun main() {
val data = OwnedProject("kotlinx.coroutines", "kotlin")
println(Json.encodeToString(data))
}
フルコードはこちらから取得することができます。
OwnedProject
クラスはシリアライズできないので、エラーが発生します。
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'OwnedProject' is not found.
Mark the class as @Serializable or provide the serializer explicitly.
シリアライズ可能な階層の設計
先ほどの例の OwnedProject
を単に @Serializable
としてマークすることはできません。これはコンパイルされず、コンストラクタのプロパティ要件に陥ってしまいます。階層化されたクラスをシリアライズ可能にするには、親クラスのプロパティに abstract
のマークを付けなければならず、Project
クラスも abstract
になってしまう。
@Serializable
abstract class Project {
abstract val name: String
}
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(Json.encodeToString(data))
}
フルコードこちらから取得することができます。
これは、シリアライズ可能なクラスの階層構造のための最良のデザインに近いですが、実行すると次のようなエラーが発生します。
Exception in thread "main" kotlinx.serialization.SerializationException: Class 'OwnedProject' is not registered for polymorphic serialization in the scope of 'Project'.
Mark the base class as 'sealed' or register the serializer explicitly.
シールドされたクラス
ポリモーフィック階層でシリアライズを利用する最も簡単な方法は、基底クラスに sealed
をマークすることです。密閉されたクラスのすべてのサブクラスは、明示的に @Serializable
としてマークしなければなりません。
@Serializable
sealed class Project {
abstract val name: String
}
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(Json.encodeToString(data))
}
フルコードはこちらから取得することができます。
これで、JSONで多態性を表現するデフォルトの方法が見えてきました。結果のJSONオブジェクトには、type
キーが_discriminator_として追加されます。
{"type":"example.examplePoly04.OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"}
カスタムサブクラスのシリアル名
type` キーの値は、デフォルトでは完全修飾されたクラス名である。これを変更するには、対応するクラスに SerialName アノテーションを付けることができます。
@Serializable
sealed class Project {
abstract val name: String
}
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(Json.encodeToString(data))
}
フルコードはこちらから取得することができます。
こうすることで、ソースコードのクラス名に影響されない安定した_serial name_を持つことができます。
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
それに加えて、JSONではクラス判別器に別のキー名を使用するように設定することができます。クラス判別器のセクションに例があります。
基底クラスの具体的なプロパティ
シールドされた階層の基底クラスは、バッキングフィールドを持つプロパティを持つことができます。
@Serializable
sealed class Project {
abstract val name: String
var status = "open"
}
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val json = Json { encodeDefaults = true } // "status" will be skipped otherwise
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(json.encodeToString(data))
}
フルコードはこちらから取得することができます。
スーパークラスのプロパティは、サブクラスのプロパティの前にシリアライズされます。
{"type":"owned","status":"open","name":"kotlinx.coroutines","owner":"kotlin"}
オブジェクト
封印された階層はサブクラスとしてオブジェクトを持つことができ、@Serializable
としてマークされている必要があります。ここでは、Response
クラスの階層を持つ別の例を見てみましょう。
@Serializable
sealed class Response
@Serializable
object EmptyResponse : Response()
@Serializable
class TextResponse(val text: String) : Response()
さまざまなレスポンスのリストをシリアライズしてみましょう。
fun main() {
val list = listOf(EmptyResponse, TextResponse("OK"))
println(Json.encodeToString(list))
}
フルコードはこちらから取得することができます。
オブジェクトは空のクラスとしてシリアライズされ、デフォルトでは完全修飾されたクラス名を型として使用します。
[{"type":"example.examplePoly07.EmptyResponse"},{"type":"example.examplePoly07.TextResponse","text":"OK"}]
オブジェクトがプロパティを持っていてもシリアル化されません。
開かれたポリモフィズム
シリアライズは任意の open
クラスや abstract
クラスで動作します。しかし、この種の多態性はオープンであるため、ソースコードのどこにでもサブクラスが定義されている可能性があり、他のモジュールであっても、シリアライズされるサブクラスのリストはコンパイル時に決定できず、実行時に明示的に登録しなければなりません。
登録されたサブクラス
まずは、シリアライズ可能な階層の設計のセクションのコードから見ていきましょう。封印せずにシリアライズで動作させるためには、SerializersModule {}ビルダー関数を使ってSerializersModuleを定義する必要があります。モジュールでは、ポリモーフィックビルダーで基底クラスを指定し、各サブクラスをサブクラス関数で登録します。これで、カスタム JSON 設定をこのモジュールでインスタンス化し、シリアライズに使用できるようになりました。
カスタムJSON設定の詳細は、JSON設定のセクションに記載されています。
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
}
}
val format = Json { serializersModule = module }
@Serializable
abstract class Project {
abstract val name: String
}
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(data))
}
フルコードをこちらから取得することができます。
この追加設定により、シールドされたクラス セクションの密閉されたクラスと同じように動作しますが、ここではサブクラスをコード全体に任意に分散させることができます。
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
この例は
serializer
関数の制限のため JVM でしか動作しないことに注意してください。JSやNativeの場合は、明示的なシリアライザformat.encodeToString(PolymorphicSerializer(Project::class), data)
を使用する必要があります。この問題については、ここらで追跡することができます。
インターフェイスのシリアル化
先ほどの例を更新して、Project
スーパークラスをインターフェイスにすることができる。 しかし、インターフェイスそのものを @Serializable
としてマークすることはできない。問題ない。インターフェースはそれ自体でインスタンスを持つことはできない。 インターフェースはその派生クラスのインスタンスでしか表現できない。インターフェースはKotlin言語ではポリモーフィズムを可能にするために使われているので、すべてのインターフェースはPolymorphicSerializerストラテジーを使って暗黙のうちに直列化可能であるとみなされます。これらの実装クラスを @Serializable
としてマークして登録するだけです。
interface Project {
val name: String
}
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project
これで、data
を Project
型で宣言すると、以前と同様に format.encodeToString
を呼び出すことができるようになりました。
fun main() {
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(data))
}
フルコードはこちらから取得することができます。
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
インターフェース型のプロパティ
先ほどの例に引き続き、Project
インターフェースを他のシリアライズ可能なクラスのプロパティとして使うとどうなるか見てみましょう。インターフェースは暗黙のうちに多態性を持っているので、インターフェース型のプロパティを宣言すればいいのです。
@Serializable
class Data(val project: Project) // Project is an interface
fun main() {
val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
println(format.encodeToString(data))
}
フルコードはこちらから取得することができます。
format
のSerializersModuleにシリアライズされるインターフェースの実際のサブタイプを登録しておけば、実行時に動作するようになる。
{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
多態性のための静的親型検索
多態性クラスのシリアライズの際には、多態性階層のルート型 (この例では Project
) が静的に決定されます。ここでは直列化可能な 抽象クラス Project
を例にとりますが、main
関数を変更して data
の型が Any
であることを宣言します。
fun main() {
val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(data))
}
完全なコードはこちらから取得できます。
以下のような例外が発生します。
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Mark the class as @Serializable or provide the serializer explicitly.
ソースコードで使用している対応するスタティック型を重視して、ポリモーフィックシリアライズのためのクラスを登録しなければなりません。まず、Any
のサブクラスを登録するようにモジュールを変更します。
val module = SerializersModule {
polymorphic(Any::class) {
subclass(OwnedProject::class)
}
}
そこでは、Any
型の変数をシリアライズしてみる。
fun main() {
val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(data))
}
フルコードはこちらから取得することができます。
しかし、Any
はクラスであり、シリアライズはできません。
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Mark the class as @Serializable or provide the serializer explicitly.
基底クラス Any
の PolymorphicSerializer のインスタンスを明示的に encodeToString 関数の最初のパラメータとして渡さなければなりません。
fun main() {
val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(PolymorphicSerializer(Any::class), data))
}
フルコードはこちらから取得することができます。
明示的なシリアライザを使用すると、以前と同じように動作します。
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
多態クラスのプロパティを明示的にマーキング
インターフェースはすべて実行時のポリモーフィズムに関するものなので、インターフェース型のプロパティは暗黙のうちにポリモーフィズムとみなされます。しかし、Kotlinのシリアライズでは、シリアライズ可能なクラスの型がシリアライズ不可能なプロパティを持つクラスをコンパイルしません。もし Any
クラスやその他のシリアライズ可能でないクラスのプロパティを持っている場合は、プロパティにシリアライザを指定のセクションで見たように、@Serializable
アノテーションを使って明示的にそのシリアライズ方法を提供しなければなりません。プロパティの多態直列化戦略を指定するには、特殊目的の @Polymorphic
アノテーションを使用します。
@Serializable
class Data(
@Polymorphic // the code does not compile without it
val project: Any
)
fun main() {
val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
println(format.encodeToString(data))
}
フルコードはこちらから取得することができます。
複数のスーパークラスの登録
同じクラスがスーパークラスのリストからコンパイル時の型が異なるプロパティの値としてシリアライズされる場合は、スーパークラスごとに個別にSerializersModuleに登録する必要があります。全てのサブクラスの登録を別の関数に抽出して、スーパークラスごとに使うと便利です。以下のようなテンプレートで記述します。
val module = SerializersModule {
fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
subclass(OwnedProject::class)
}
polymorphic(Any::class) { registerProjectSubclasses() }
polymorphic(Project::class) { registerProjectSubclasses() }
}
フルコードはこちらから取得することができます。
多態性とジェネリッククラス
シリアライズ可能なクラスの汎用サブタイプは、特別な処理を必要とします。以下の階層を考えてみましょう。
@Serializable
abstract class Response<out T>
@Serializable
@SerialName("OkResponse")
data class OkResponse<out T>(val data: T) : Response<T>()
Kotlinのシリアライズには、多態型 OkResponse<T>
のプロパティをシリアライズする際に、型パラメータ T
に対して実際に提供される引数の型を表現するためのストラテジーが組み込まれていません。このストラテジーは、Response
用のシリアライザモジュールを定義する際に明示的に指定する必要があります。以下の例では、OkResponse.serializer(....)
を使って OkResponse
クラスの プラグイン生成ジェネリックシリアライザ を作成し、Any
クラスをベースにして PolymorphicSerializer (https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-polymorphic-serializer/index.html) インスタンスを作成します。これにより、OkResponse
のインスタンスを、Any
のサブタイプとしてポリモーフィックに登録された任意のdata
プロパティでシリアライズすることができます。
val responseModule = SerializersModule {
polymorphic(Response::class) {
subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
}
}
ライブラリのシリアライザモジュールのマージ
アプリケーションのサイズが大きくなり、ソースコードモジュールに分割された場合、1つのシリアライザモジュールにすべてのクラス階層を格納するのは不便になるかもしれません。前節のコードにProject
階層を持つライブラリを追加してみよう。
val projectModule = SerializersModule {
fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
subclass(OwnedProject::class)
}
polymorphic(Any::class) { registerProjectSubclasses() }
polymorphic(Project::class) { registerProjectSubclasses() }
}
plus演算子を使ってこれら2つのモジュールを結合することで、同じJson形式のインスタンスで両方のモジュールを使用することができます。
SerializersModule {} DSL の include 関数を使用することもできます。
val format = Json { serializersModule = projectModule + responseModule }
これで、両方の階層のクラスを一緒にシリアライズし、一緒にデシリアライズすることができるようになりました。
fun main() {
// both Response and Project are abstract and their concrete subtypes are being serialized
val data: Response<Project> = OkResponse(OwnedProject("kotlinx.serialization", "kotlin"))
val string = format.encodeToString(data)
println(string)
println(format.decodeFromString<Response<Project>>(string))
}
完全なコードはこちらから取得できます。
生成されているJSONは深いポリモーフィックを持っています。
{"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}}
OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin))
抽象クラスとそれのいくつかの実装を持つライブラリや共有モジュールを書いている場合、クライアントが自分のモジュールと自分のモジュールを組み合わせることができるように、クライアントが使用できるように自分のシリアライザモジュールを公開することができます。
デシリアライゼーションのためのデフォルトの多態型ハンドラ
登録されていなかったサブクラスをデシリアライズすると、どうなるのでしょうか?
fun main() {
println(format.decodeFromString<Project>("""
{"type":"unknown","name":"example"}
"""))
}
フルコードはこちらから取得することができます。
以下のような例外が発生します。
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator 'unknown'
フレキシブルな入力を読み込む際には、この場合のデフォルトの動作を提供したいと思うかもしれません。例えば、BasicProject
のサブタイプを持つことで、あらゆる種類の未知のProject
サブタイプを表現することができます。
@Serializable
abstract class Project {
abstract val name: String
}
@Serializable
data class BasicProject(override val name: String, val type: String): Project()
@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()
デフォルトハンドラを登録するには、polymorphic { ... }
DSLのdefault
関数を使用します。以下の例では、typeは使用せず、常に BasicProject
クラスの Plugin-generated serializer を返しています。 デフォルトハンドラは、入力のtype
文字列をdeserialization strategyにマップするストラテジーを定義するpolymorphic { ... }
DSLのdefault
関数を使用して登録します。以下の例では、型は使わず、常に BasicProject
クラスの Plugin-generated serializer を返すようにしています。
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
default { BasicProject.serializer() }
}
}
このモジュールを使うと、登録された OwnedProject
のインスタンスと未登録のインスタンスの両方をデシリアライズすることができます。
val format = Json { serializersModule = module }
fun main() {
println(format.decodeFromString<List<Project>>("""
[
{"type":"unknown","name":"example"},
{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}
]
"""))
}
フルコードはこちらから取得することができます。
BasicProjectは
type`プロパティで指定された型のキーも取得していることに注目してください。
[BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)]
プラグインで生成されたシリアライザをデフォルトのシリアライザとして使用し、「未知の」データの構造が事前にわかっていることを暗示しています。実際のAPIでは、このようなことはほとんどありません。そのためには、構造化されていないカスタムのシリアライザが必要です。
このようなシリアライザの例は、後述のカスタムJSON属性のメンテナンスの項でご紹介します。
次の章では、JSONの機能を取り上げます。