Kotlin serializationで多態なJSONを入出力する

# やりたいこと

```json:JSON
[
  {
    "id": "1",
    "name": "Alice",
    "status": "thinking"
  },
  {
    "id": "2",
    "name": "Bob",
    "status": {
      "label": "考え中",
      "emoji": ":thinking_face:"
    }
  }
]
```

上記のように、`status` の値の型に多態性がある JSON 文字列を、 [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) を使って以下のようなクラスからシリアライズ／にデシリアライズしたい。

```kotlin
data class User(
    val id: String,
    val name: String,
    val status: Status,
)

sealed class Status {
    data class TextStatus(
        val textValue: String
    ) : Status()
    data class StructuredStatus(
        val label: String,
        val emoji: String,
    ) : Status()
}
```

# JSON String と 独自クラス 間のシリアライズ

多態性のために `val status: Status` のところを `val status: String` と定義できない以上、どうにかして JSON String の値を `Status` のサブクラスである `TextStatus` に変換する必要がある。

そのためには [カスタムシリアライザー](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers) を作る。

```kotlin
@Serializable(with = TextStatusSerializer::class)
data class TextStatus(
    val textValue: String,
) : Status()

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

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

    override fun deserialize(decoder: Decoder): TextStatus {
        return TextStatus(textValue = decoder.decodeString())
    }
}
```

こういうカスタムシリアライザーを `@Serializable` の `with` で指定すれば、 `status: "thinking"` という JSON 文字列が `StringStatus(stringValue=thinking)` にデシリアライズ（その逆の、 data class から JSON 文字列にシリアライズも）できる。

# コンテンツに合わせて別のクラスにデシリアライズする

今回扱いたい JSON をデシリアライズするとき、 JSON 上には `status` の型を明示する識別子のようなものはないので、 JSON 上の値やその型を直接みて `TextStatus` と `StructuredStatus` を使い分ける必要がある。ここでもカスタムシリアライザーを使うが、 [JsonContentPolymorphicSerializer](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#content-based-polymorphic-deserialization) を利用する。

```kotlin
@Serializable(with = StatusSerializer::class)
sealed class Status

object StatusSerializer : JsonContentPolymorphicSerializer<Status>(Status::class) {
    override fun selectDeserializer(element: JsonElement) = when {
        element is JsonObject -> StructuredStatus.serializer()
        element is JsonPrimitive && element.isString -> TextStatus.serializer()
        else -> throw IllegalArgumentException("unknown type")
    }
}
```

これで、 `"status": "thinking"` は `StringStatus(stringValue=thinking)` に、 `"status": { "label": "考え中", "emoji": ":thinking_face:" }` は `StructuredStatus(label=考え中, emoji=:thinking_face:)` にデシリアライズされる。

また、 JsonContentPolymorphicSerializer を使っていることで、逆のJSONへのシリアライズにおいても、適切に各フォーマットで出力される。


# ここまでのまとめ

```kotlin
@Serializable
data class User(
    val id: String,
    val name: String,
    val status: Status,
)

@Serializable(with = StatusSerializer::class)
sealed class Status

@Serializable(with = TextStatusSerializer::class)
data class TextStatus(
    val textValue: String,
) : Status()

@Serializable
data class StructuredStatus(
    val label: String,
    val emoji: String,
) : Status()

// 各カスタムシリアライザーの実装は省略

// 以下、テスト

val jsonString = """[{"id":"1","name":"Alice","status":"thinking"},{"id":"2","name":"Bob","status":{"label":"考え中","emoji":":thinking_face:"}}]"""
val decodedUsers: List<User> = Json.decodeFromString(jsonString)

val users = listOf(
    User(id = "1", name = "Alice", status = TextStatus(textValue = "thinking")),
    User(id = "2", name = "Bob", status = StructuredStatus(label = "考え中", emoji = ":thinking_face:")),
)
val encodedJsonString = Json.encodeToString(users)

assert(users == decodedUsers)
assert(jsonString == encodedJsonString)
```

このように、カスタムシリアライザーを実装することで、多態性のあるフォーマットの JSON 文字列を、 sealed class との間で変換することが出来た。

# 余談: JSON の中に型を明示する識別子があれば、話は簡単

もし JSON フォーマットが、以下のように

- `status` の値が必ず JSON Object である
- その中に、型を識別する文字列が `status_type` として必ず存在する

となっていれば、カスタムシリアライザーなしに実装可能。

```json:JSON
[
  {
    "id": "1",
    "name": "Alice",
    "status": {
      "status_type": "text",
      "text_value": "thinking"
    }
  },
  {
    "id": "2",
    "name": "Bob",
    "status": {
      "status_type": "label_with_emoji",
      "label": "考え中",
      "emoji": ":thinking_face:"
    }
  }
]
```

ベースクラスに `@JsonClassDiscriminator`、サブクラスに `@SerialName` で それぞれ識別のキーと値を指定することで、カスタムシリアライザーなしに、自動的に各サブクラスにマッピングされる。

```kotlin
@Serializable
data class User(
    val id: String,
    val name: String,
    val status: Status
)

@Serializable
@JsonClassDiscriminator("status_type")
sealed class Status

@Serializable
@SerialName("text")
data class TextStatus(
    @SerialName("text_value") val textValue: String,
) : Status()

@Serializable
@SerialName("label_with_emoji")
data class StructuredStatus(
    @SerialName("label") val label: String,
    @SerialName("emoji") val emoji: String,
) : Status()

// これだけ。
// 以下、テスト。

val jsonString = """[{"id":"1","name":"Alice","status":"thinking"},{"id":"2","name":"Bob","status":{"label":"考え中","emoji":":thinking_face:"}}]"""
val decodedUsers: List<User> = Json.decodeFromString(jsonString)

val users = listOf(
    User(id = "1", name = "Alice", status = TextStatus(textValue = "thinking")),
    User(id = "2", name = "Bob", status = StructuredStatus(label = "考え中", emoji = ":thinking_face:")),
)
val encodedJsonString = Json.encodeToString(users)

assert(users == decodedUsers)
assert(jsonString == encodedJsonString)
```

この JSON フォーマットであれば例えば TypeScript の場合は以下のように discriminated union を使って実装するのだろうし、仕様としてはこちらのほうが扱いやすいのだろう。

```typescript:TypeScript
type User = {
  id: string
  name: string
  status: Status
}

type Status = TextStatus | StructuredStatus

type TextStatus = {
  status_type: "text"
  text_value: string
}

type StructuredStatus = {
  status_type: "label_with_emoji"
  label: string
  emoji: string
}
```

# 動作環境

- Kotlin: 1.6.21
- kotlinx-serialization-json: 1.3.3

# 参考リンク

- [Kotlin Serialization Guide](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md)
  - ここ見ればだいたい欲しい情報は載ってる
  - 今回は特に、[Serializers](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md), [Polymorphism](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md), [JSON features](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md) を参照した