kotlinx.serializationでやりたいことを調査したので、その結果をtipsとしてまとめました
versionはこちらを使っています
kotlin("plugin.serialization") version "1.9.22"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
1. 日時をZonedDateTimeに変換したい
Custom Serializerを作って、Serializable AnnotationでCustom Serializerを指定してあげればOKです
object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ZonedDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: ZonedDateTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): ZonedDateTime = ZonedDateTime.parse(decoder.decodeString())
}
@Serializable
data class Response(
@Serializable(ZonedDateTimeSerializer::class)
val dateUtc: ZonedDateTime,
@Serializable(ZonedDateTimeSerializer::class)
val dateJst: ZonedDateTime,
)
val json =
"""
{
"dateUtc": "2024-02-17T02:03:26Z",
"dateJst": "2024-02-17T11:03:26+09:00"
}
"""
val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
val string = Json.encodeToString(Response.serializer(), response)
plintln(string)
Response(dateUtc=2024-02-17T02:03:26Z, dateJst=2024-02-17T11:03:26+09:00)
{"dateUtc":"2024-02-17T02:03:26Z","dateJst":"2024-02-17T11:03:26+09:00"}
2. Sealed Interfaceを実装したData Classに変換したい
Polymorphismに対応しているため簡単にできます
SerialName Annotationでproperty typeのvalueを指定してあげればOKです
※ defaultではpropertyはtypeである必要があります(customのやり方は4でかきます)
@Serializable
data class Response(
val firstNote: Note,
val secondNote: Note,
)
@Serializable
sealed interface Note{
val text: String
}
@Serializable
@SerialName("A")
data class NoteA(
override val text: String,
) : Note
@Serializable
@SerialName("B")
data class NoteB(
override val text: String,
val number: Int,
) : Note
val json =
"""
{
"firstNote": {
"text": "text",
"number": 1,
"type": "B"
},
"secondNote": {
"text": "text",
"type": "A"
}
}
"""
val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
Response(firstNote=NoteB(text=text, number=1), secondNote=NoteA(text=text))
3. 2をlistで持ちたい
すみません、分けたのですが、2と全く同じです
@Serializable
data class Response(
val notes: List<Note>,
)
@Serializable
sealed interface Note{
val text: String
}
@Serializable
@SerialName("A")
data class NoteA(
override val text: String,
) : Note
@Serializable
@SerialName("B")
data class NoteB(
override val text: String,
val number: Int,
) : Note
val json =
"""
{
"notes": [
{
"text": "text!!",
"number": 22,
"type": "B"
},
{
"text": "text",
"type": "A"
}
]
}
"""
val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
Response(notes=[NoteB(text=text!!, number=22), NoteA(text=text)])
4. 2と3のpropertyをcustomしたい
sealed interfaceにJsonClassDiscriminator Annotationでpropertyのkeyを指定してあげればOKです
※ sealed interface だと何故か型解決できなかったため、この場合はsealed classにしてください
@Serializable
data class Response(
val note: Note,
val notes: List<Note>,
)
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("customType")
sealed class Note{
abstract val text: String
}
@Serializable
@SerialName("A")
data class NoteA(
override val text: String,
) : Note()
@Serializable
@SerialName("B")
data class NoteB(
override val text: String,
val number: Int,
) : Note()
val json =
"""
{
"note": {
"text": "textB",
"customType": "B",
"number": 99
},
"notes": [
{
"text": "text!!",
"number": 22,
"customType": "B"
},
{
"text": "text",
"customType": "A"
}
]
}
"""
val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
Response(note=NoteB(text=textB, number=99), notes=[NoteB(text=text!!, number=22), NoteA(text=text)])
5. 2と3でpropertyに未知のvalueがあったら無視したい
調べたのですが、Annotationでは解決できなさそうでした
(もし、ご存知の方がいましたら教えてください🙇🏻♂️)
そのため、Custom Serializerを作成して、未知のtypeはnullを返すようにしてあげればよさそうです
(PolymorphismのAnnotationは削除します)
object NoteSerializer : KSerializer<Note?> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Note", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Note? {
val input = decoder as JsonDecoder
val tree = input.decodeJsonElement()
return when (tree.jsonObject["type"]?.jsonPrimitive?.contentOrNull) {
"A" -> Json.decodeFromJsonElement(NoteA.serializer(), tree)
"B" -> Json.decodeFromJsonElement(NoteB.serializer(), tree)
else -> null
}
}
override fun serialize(encoder: Encoder, value: Note?) {
when (value) {
is NoteA -> encoder.encodeSerializableValue(NoteA.serializer(),value)
is NoteB -> encoder.encodeSerializableValue(NoteB.serializer(),value)
else -> {/* noop */}
}
}
}
@Serializable
data class Response(
val note1: Note? = null,
val note2: Note? = null,
val notes: List<Note?>,
)
@Serializable(with = NoteSerializer::class)
sealed class Note {
abstract val text: String
abstract val type: String
}
@Serializable
data class NoteA(
override val text: String,
override val type: String,
) : Note()
@Serializable
data class NoteB(
override val text: String,
override val type: String,
val number: Int,
) : Note()
val json =
"""
{
"note1": {
"text": "textB",
"type": "B",
"number": 99
},
"note2": {
"text": "textX",
"type": "X",
"number": 87
},
"notes": [
{
"text": "text!!",
"number": 22,
"type": "Z"
},
{
"text": "text",
"type": "A"
}
]
}
"""
val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
val string = Json.encodeToString(Response.serializer(), response)
plintln(string)
Response(note1=NoteB(text=textB, type=B, number=99), note2=null, notes=[null, NoteA(text=text, type=A)])
{"note1":{"text":"textB","type":"B","number":99},"notes":[null,{"text":"text","type":"A"}]}
filteringすればいいですが、listにnullが入ってくるのは正直ちょっと嫌ですね・・
listもCustom Serializerを作ればいいのかな?