はじめに
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を実装します。
@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()
}
{
id: "00",
name: "プリン",
price: {
min: 200,
max: 500
},
type: "cold",
storageTemperature: 10
}
{
id: "01",
name: "あんまん",
price: {
min: 300,
max: 600
},
type: "hot",
warmingTime: 300
}
レスポンスは、Coldなら保存温度(Int)、Hotなら温め時間(Long)が返ってくるものとします。
レスポンスのクラスとしてはSweetsを受け取りたいが、typeによってCold/Hotにパースしたいですね。
このようなSerializerで実現できます。
@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を利用します。
@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を筋肉で解決することもできます(運用観点で良くない実装かと)
@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もパースできることが分かったので、様々なケースに対応できそうです。