3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Kotlin Serialization ガイド 第4章 ポリモフィズム

Last updated at Posted at 2021-02-26

#はじめに
 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

 これで、dataProject 型で宣言すると、以前と同様に 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))
}        

フルコードはこちらから取得することができます。

 formatSerializersModuleにシリアライズされるインターフェースの実際のサブタイプを登録しておけば、実行時に動作するようになる。

{"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.

 基底クラス AnyPolymorphicSerializer のインスタンスを明示的に 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"} 
        ]
    """))
}

フルコードはこちらから取得することができます。

 BasicProjecttype`プロパティで指定された型のキーも取得していることに注目してください。

[BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)]

 プラグインで生成されたシリアライザをデフォルトのシリアライザとして使用し、「未知の」データの構造が事前にわかっていることを暗示しています。実際のAPIでは、このようなことはほとんどありません。そのためには、構造化されていないカスタムのシリアライザが必要です。
このようなシリアライザの例は、後述のカスタムJSON属性のメンテナンスの項でご紹介します。


次の章では、JSONの機能を取り上げます。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?