LoginSignup
5
3

kotlinx.serialization tips

Last updated at Posted at 2024-02-17

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です

Custom Serializer
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())
}
Data Class
@Serializable
data class Response(
    @Serializable(ZonedDateTimeSerializer::class)
    val dateUtc: ZonedDateTime,
    @Serializable(ZonedDateTimeSerializer::class)
    val dateJst: ZonedDateTime,
)
Execute
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)
Result
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でかきます)

Data Class
@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
Execute
val json =
    """
    {
      "firstNote": {
        "text": "text",
        "number": 1,
        "type": "B"
      },
      "secondNote": {
        "text": "text",
        "type": "A"
      }
    }
    """
val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
Result
Response(firstNote=NoteB(text=text, number=1), secondNote=NoteA(text=text))

3. 2をlistで持ちたい

すみません、分けたのですが、2と全く同じです

Data Class
@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
Execute
val json =
    """
    {
      "notes": [
        {
          "text": "text!!",
          "number": 22,
          "type": "B"
        },
        {
          "text": "text",
          "type": "A"
        }
      ]
    }
    """

val response: Response = Json.decodeFromString<Response>(json)
plintln(response.toString())
Result
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にしてください

Data 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()
Execute
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())
Result
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は削除します)

Custom Serializer
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 */}
        }
    }
}
Data Class
@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()
Execute
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)
Result
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を作ればいいのかな?

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