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))