2

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.

Adam Chance
  • 277
  • 1
  • 2
  • 10
  • Have a look through the docs on ['closed polymorphism'](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#closed-polymorphism). EDIT: I've had a closer look, and I don't think there's a built-in solution for what you need. – aSemy Nov 18 '22 at 12:06
  • Since you're working with JSON you can try using [content based polymorphic deserialization](https://github.com/Kotlin/kotlinx.serialization/blob/v1.4.1/docs/json.md#content-based-polymorphic-deserialization) – aSemy Nov 18 '22 at 12:15
  • @aSemy Will try this and let you know how I get on, thanks – Adam Chance Nov 18 '22 at 12:25

1 Answers1

2

tl;dr: skip to the full example at the bottom

Problem summary

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.

JSON content based polymorphic deserialization

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

Complete example

Here's a complete, runnable example, with all the imports.

I made a couple of tweaks to your code.

  • I made InteractionData a sealed interface, because it seemed appropriate
  • I converted the classes to data classes, so Kotlin generates a nice toString().
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")
    }
  }
}

Output

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

Versions

  • Kotlin 1.7.21
  • Kotlinx Serialization 1.4.1
aSemy
  • 5,485
  • 2
  • 25
  • 51