I am trying to work with a REST API that returns a JSON document who's structure depends on the value of a property named type.
I have defined the main class as follows:
@Serializable class Interaction(
val type: Byte,
val data: InteractionData? = null
)
The structure of InteractionData depends on the value of type. This is currently an interface that the four possible structures inherit from.
If type equals 2, data should be a class named ApplicationCommandData:
@Serializable class ApplicationCommandData(
val id: String,
val name: String
): InteractionData
If type equals 3, data should be a class named MessageComponentData:
@Serializable class MessageComponentData(
val custom_id: String
): InteractionData
How can I make it so that the data property is serialised as the correct class based on the value of the type property?
I have tried setting the data property to @Transient, checking the value of type, and creating a new variable with @SerialName set to data inside of the class init block but @SerialData is not valid for local variables.
tl;dr: skip to the full example at the bottom
You have a polymorphic class, and the type is determined by a property outside of the class.
{
"type": 2, <- extract this
"data": { <- determined by 'type'
"id": "0001",
"name": "MEGATRON"
}
}
Kotlinx Serialization provides the tools to handle this - but they need some assembly.
Since you're working with JSON this is possible using content based polymorphic deserialization.
Here's an initial implementation, but there's a flaw...
object InteractionJsonSerializer : JsonContentPolymorphicSerializer<Interaction>(
Interaction::class
) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Interaction> {
// extract the type from the plain JSON object
val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull
println("found InteractionData type: $type")
return when (type) {
// can't specify the type of InteractionData
2 -> Interaction.serializer()
3 -> Interaction.serializer()
else -> error("unknown type $type")
}
}
}
It's not possible to select a specific serializer, because Interaction doesn't have a type parameter, so let's add one.
@Serializable
data class Interaction<T : InteractionData?>( // add a type parameter
val type: Byte,
val data: T? = null
)
Now the Kotlinx Serialization plugin will generate a serializer that accepts a serializer for T: InteractionData. We can update InteractionJsonSerializer to make use of this.
object InteractionJsonSerializer : JsonContentPolymorphicSerializer<Interaction<*>>(
Interaction::class
) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Interaction<*>> {
val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull
println("found InteractionData type: $type")
return when (type) {
// now the type can be specified
2 -> Interaction.serializer(ApplicationCommandData.serializer())
3 -> Interaction.serializer(MessageComponentData.serializer())
else -> error("unknown type $type")
}
}
}
Here's a complete, runnable example, with all the imports.
I made a couple of tweaks to your code.
InteractionData a sealed interface, because it seemed appropriatetoString().import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
fun main() {
val interactionType2 =
Json.decodeFromString(
InteractionJsonSerializer,
/*language=JSON*/
"""
{
"type": 2,
"data": {
"id": "0001",
"name": "MEGATRON"
}
}
""".trimIndent()
)
println(interactionType2)
val interactionType3 =
Json.decodeFromString(
InteractionJsonSerializer,
/*language=JSON*/
"""
{
"type": 3,
"data": {
"custom_id": "abc123"
}
}
""".trimIndent()
)
println(interactionType3)
}
@Serializable
data class Interaction<T : InteractionData?>(
val type: Byte,
val data: T? = null
)
sealed interface InteractionData
@Serializable
data class ApplicationCommandData(
val id: String,
val name: String
) : InteractionData
@Serializable
data class MessageComponentData(
val custom_id: String
) : InteractionData
object InteractionJsonSerializer : JsonContentPolymorphicSerializer<Interaction<*>>(
Interaction::class
) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Interaction<*>> {
val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull
println("found InteractionData type: $type")
return when (type) {
2 -> Interaction.serializer(ApplicationCommandData.serializer())
3 -> Interaction.serializer(MessageComponentData.serializer())
else -> error("unknown type $type")
}
}
}
found InteractionData type: 2
Interaction(type=2, data=ApplicationCommandData(id=0001, name=MEGATRON))
found InteractionData type: 3
Interaction(type=3, data=MessageComponentData(custom_id=abc123))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With