Kotlin Sealed Classで多態的なJSONをモデリングする

型によって形状が変わる多態的なJSON構造を表現するためにKotlin sealed classを使用する方法を学びます。ディスクリミネーターフィールドと網羅的パターンマッチングを解説します。

Advanced Patterns

詳細な説明

Sealed Classによる多態的なJSON

JSONフィールドがディスクリミネーター("type"など)に応じて異なる形状のオブジェクトを含む場合、Kotlin sealed classは網羅的パターンマッチング付きの型安全な表現を提供します。

JSONの例

{
  "type": "text",
  "content": "Hello, world!"
}
{
  "type": "image",
  "url": "https://example.com/photo.jpg",
  "width": 800,
  "height": 600
}

生成されるKotlin

@Serializable
sealed class Message {
    abstract val type: String

    @Serializable
    @SerialName("text")
    data class Text(
        override val type: String = "text",
        val content: String
    ) : Message()

    @Serializable
    @SerialName("image")
    data class Image(
        override val type: String = "image",
        val url: String,
        val width: Int,
        val height: Int
    ) : Message()
}

網羅的なWhen式

fun render(message: Message) = when (message) {
    is Message.Text -> renderText(message.content)
    is Message.Image -> renderImage(message.url, message.width, message.height)
    // サブタイプが処理されていないとコンパイルエラー
}

kotlinx.serializationの多態性

val module = SerializersModule {
    polymorphic(Message::class) {
        subclass(Message.Text::class)
        subclass(Message.Image::class)
    }
}

val json = Json {
    serializersModule = module
    classDiscriminator = "type"
}

Sealed Class vs Enum Classの使い分け

特徴 Enum Sealed Class
固定の値セット はい はい
各バリアントが異なるデータを保持 いいえ はい
パターンマッチング はい はい
標準でシリアライズ可能 はい 設定が必要

バリアントが単なるラベルの場合はenumを使用します。各バリアントが異なる構造を持つ場合はsealed classを使用します。

Sealed Interface (Kotlin 1.5+)

sealed interface Event {
    data class Click(val x: Int, val y: Int) : Event
    data class KeyPress(val key: String) : Event
    data object Logout : Event
}

Sealed interfaceはクラスが複数のsealed階層を実装できるため、sealed classよりも柔軟性があります。

ユースケース

チャットアプリ、通知システム、イベント駆動アーキテクチャは形状が変わるJSONペイロードを受信します。Sealed classはすべてのバリアントがコンパイル時に処理されることを保証し、Android UIレンダリングやサーバーイベント処理でのケース漏れバグを排除します。

試してみる — JSON to Kotlin

フルツールを開く