LoginSignup
8
5

More than 3 years have passed since last update.

kotlinx.serializationでCustomSerializerを作り、ライブラリクラス・sealed classのdeserializeを行う

Last updated at Posted at 2020-10-22

はじめに

kotlinx.serializationの1.0.0(stable)が出ましたね!
以前からRetrofitと組み合わせて通信レスポンスのマッピングに利用していたのですが、CustomSerializerを作りたくなって調べて試したのでそのメモです。
※あくまで自分の理解なので、足りていない部分やパフォーマンスが良くない実装があるかもしれません。
Serializerについてのメモなので、serializationの導入などは省きます。

利用バージョン
org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0

1. あるJsonオブジェクトをライブラリのクラスにパースして使いたい

あまり遭遇するケースでは無いかもしれませんが、Serializerの手始めとして。

パースしたい通信レスポンス
{
    id: "00",
    name: "プリン",
    price: {
        min: 200,
        max: 500
    }
}
ライブラリクラス
class Price(
    val priceMin: Int,
    val priceMax: Int
)
レスポンスのデータクラス
@Serializable
data class Sweets(
    val id: String,
    val name: String,
    val price: Price
)

このとき、レスポンスをSweetsにデシリアライズしたいが、Priceがライブラリクラスなので@Serializableを付けられません。
しかも、jsonとPriceクラスで変数名も異なっています。
これを解決するには、KSerializerを継承したカスタムSerializerを実装します。

PriceSerializer.kt
@Serializer(forClass = Price::class)
object PriceSerializer : KSerializer<Price> {

    override fun deserialize(decoder: Decoder): Price {
        require(decoder is JsonDecoder)
        val element = decoder.decodeJsonElement()
        require(element is JsonObject)
        val min = requireNotNull(element["min"]).jsonPrimitive.int
        val max = requireNotNull(element["max"]).jsonPrimitive.int
        return Price(min, max)
    }

    override fun serialize(encoder: Encoder, value: Price) {
        // Serialization is not supported
    }
}

decoder.decodeJsonElementをJsonObjectにcastできるので、それでSweetsのpriceのjsonを持ってこれます。
priceが根っこなので、elementに対して入っているjsonキーを指定すれば値を取ってくることができます。
※取ってきたい型がstringの場合は、contentで取れます(toStringだとダブルクォートで括られてしまった)

serializeについては今回利用しないので空実装ですが、定義を書かないと Serializable class must have single primary constructorとコンパイルエラーになるので定義は必要でした。
(requireなどは無理やりなので、もっといい形を考えてもいいかもしれません)

利用側
@Serializable
data class Sweets(
    val id: String,
    val name: String,
    @Serializable(with = PriceSerializer::class)
    val price: Price
)

作ったSerializerをdata classの対象フィールドにwithを利用して定義すれば、デシリアライズすることができます。
これで、通信レスポンスをパースすることができます。

2. sealed classのdeserializeをしたい

レスポンスオブジェクトにtypeが入っていて、そのtypeのsealed classにパースしたい場合です。
SweetsをHotとColdのsealed classにする場合を例にします。
前項のPriceは引き続き利用します。

想定クラス
sealed class Sweets {
    abstract val id: String
    abstract val name: String
    abstract val price: Price

    data class Cold(
        override val id: String,
        override val name: String,
        override val price: Price,
        // Coldは保存温度を持つ
        val storageTemperature: Int
    ) : Sweets()

    data class Hot(
        override val id: String,
        override val name: String,
        override val price: Price,
        // Hotは温め時間を持つ
        val warmingTime: Long
    ) : Sweets()
}

通信レスポンス(Cold)
{
    id: "00",
    name: "プリン",
    price: {
        min: 200,
        max: 500
    },
    type: "cold",
    storageTemperature: 10
}
通信レスポンス(Hot)
{
    id: "01",
    name: "あんまん",
    price: {
        min: 300,
        max: 600
    },
    type: "hot",
    warmingTime: 300
}

レスポンスは、Coldなら保存温度(Int)、Hotなら温め時間(Long)が返ってくるものとします。
レスポンスのクラスとしてはSweetsを受け取りたいが、typeによってCold/Hotにパースしたいですね。

このようなSerializerで実現できます。

SweetsSerializer
@Serializer(forClass = Sweets::class)
object SweetsSerializer : KSerializer<Sweets> {

    override fun deserialize(decoder: Decoder): Sweets {
        require(decoder is JsonDecoder)
        val element = decoder.decodeJsonElement()
        require(element is JsonObject)
        val serializer = when (val type = element["type"]?.jsonPrimitive?.content) {
            "cold" -> Sweets.Cold.serializer()
            "hot" -> Sweets.Hot.serializer()
            else -> throw IllegalStateException("Sweets type is not match $type")
        }
        return decoder.json.decodeFromJsonElement(serializer, element)
    }

    override fun serialize(encoder: Encoder, value: Sweets) {
        // Serialization is not supported
    }
}

前項と同じ様に、JsonObjectから"type"が取れるので、それを元にSerializerを分けています。
一度decodeしてからserializerを用いる場合は、
decoder.json.decodeFromJsonElement(serializer, element)
を用いることで変換できるようです。
sealedの具象クラスのserializer()は、具象クラスに@Serializableをつけることで利用できます。(全体のSweetsクラスは後述)

(一度decodeしたものをdecoder.decodeSerializableValue(serializer())で変換できるかと思いきや、再利用はできない仕様でした。JsonParser.ktのreadObjectでreaderをnextしていて、一度使うと最後が参照されている模様)

Sweetsでは上記Serializerを利用します。

Sweets
@Serializable(with = SweetsSerializer::class)
sealed class Sweets {
    abstract val id: String
    abstract val name: String
    abstract val price: Price

    @Serializable
    data class Cold(
        override val id: String,
        override val name: String,
        @Serializable(with = PriceSerializer::class)
        override val price: Price,
        // Coldは保存温度を持つ
        val storageTemperature: Int
    ) : Sweets()

    @Serializable
    data class Hot(
        override val id: String,
        override val name: String,
        @Serializable(with = PriceSerializer::class)
        override val price: Price,
        // Hotは温め時間を持つ
        val warmingTime: Long
    ) : Sweets()
}

sealed classに@Serializable(with = SweetsSerializer::class)をつけるだけです。
中にCustomSerializer(PriceSerializer)が入っていても、無事パースすることができました。

蛇足

decorderからjsonObjectを見られるので、sealed classを筋肉で解決することもできます(運用観点で良くない実装かと)

MuscleSweetsSerializer
@Serializer(forClass = Sweets::class)
object MuscleSweetsSerializer : KSerializer<Sweets> {

    override fun deserialize(decoder: Decoder): Sweets {
        require(decoder is JsonDecoder)
        val element = decoder.decodeJsonElement()
        require(element is JsonObject)
        val id = element["id"]!!.jsonPrimitive.content
        val name = element["name"]!!.jsonPrimitive.content
        val priceMin = element["price"]!!.jsonObject["min"]!!.jsonPrimitive.int
        val priceMax = element["price"]!!.jsonObject["max"]!!.jsonPrimitive.int
        return when (val type = element["type"]?.jsonPrimitive?.content) {
            "cold" -> {
                val storageTemperature = element["storageTemperature"]!!.jsonPrimitive.int
                Sweets.Cold(id, name, Price(priceMin, priceMax), storageTemperature)
            }
            "hot" -> {
                val warmingTime = element["warmingTime"]!!.jsonPrimitive.long
                Sweets.Hot(id, name, Price(priceMin, priceMax), warmingTime)
            }
            else -> throw IllegalStateException("Sweets type is not match $type")
        }
    }
//...省略

serializer()を使っていきたいですね。笑

参考

公式のdocsに色々書いてあります
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md
docs一覧(種類が多い)
https://github.com/Kotlin/kotlinx.serialization/tree/master/docs

KSerializerのフィールドであるdescriptorは今回実装していませんが、jsonフィールドをマッピングしてその位置によって変換する場合に使えるのかな?と思ってます。
サンプルでは実装してあるものもあるが、利用していないことが多い(なくても動いていそう)
まだまだ理解が足りていないので、「実装すべき!」「パフォーマンス上がるよ!」などありましたらアドバイスいただけると幸いです。

最後に

Serializerを利用すると、アプリで通信後のロジックを実装する際に意味のあるクラスとして扱えるので有用だと思います。
CustomSerializerでライブラリクラスやsealed classもパースできることが分かったので、様々なケースに対応できそうです。

8
5
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
8
5