Search code examples
kotlinserializationdeserialization

Kotlin Serializable Any item


I encountered a problem with data serialization. The REST API is not mine and I can't change it. For a certain request I receive a Object:

data class State(
   val instance: String,
   val value: Any
)

Various types of data can come as the value parameter of an object, such as Boolean, String, Int, Float and even Object, for exemple:

"state": {
   "instance": "rgb",
   "value": 13910520    
}
// OR
"state": {
   "instance": "hsv",
   "value": {
      "h": 255,
      "s": 100,
      "v": 50
   }
}

In this case, the value type depends on instance (instance is always a String type). With the help of this answer, I was more or less able to understand the various types, but I did not understand how I should work if receive an Object in response.

@Serializable(with = StateSerializer::class)
data class StateObject(
    val instance: String,
    val value: Any
)
object StateSerializer : KSerializer<StateObject> {
    private val dataTypeSerializers: Map<String, KSerializer<Any>> =
        mapOf(
            "..." to serialDescriptor<Boolean>(),
            "..." to serialDescriptor<String>(),
            "..." to serialDescriptor<Int>()
        ).mapValues { (_, v) -> v as KSerializer<Any> }

    private fun getValueSerializer(instance: String): KSerializer<Any> =
        dataTypeSerializers[instance] ?: throw SerializationException()

    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StateObject") {
        element("instance", serialDescriptor<String>())
        element("value", buildClassSerialDescriptor("Any"))
    }

    override fun deserialize(decoder: Decoder): StateObject = decoder.decodeStructure(
        descriptor) {
        if (decodeSequentially()) {
            val instance = decodeStringElement(descriptor, 0)
            val value = decodeSerializableElement(
                descriptor,
                1,
                getValueSerializer(instance)
            )
            StateObject(instance, value)
        } else {
            require(decodeElementIndex(descriptor) == 0) {  }
            val instance = decodeStringElement(descriptor, 0)
            val value = when (val index = decodeElementIndex(descriptor)) {
                1 -> decodeSerializableElement(descriptor, 1, getValueSerializer(instance))
                CompositeDecoder.DECODE_DONE -> throw SerializationException("value field is missing")
                else -> error("Unexpected index: $index")
            }
            StateObject(instance, value)
        }
    }

    override fun serialize(encoder: Encoder, value: StateObject) {
        encoder.encodeStructure(descriptor) {
            encodeStringElement(descriptor, 0, value.instance)
            encodeSerializableElement(
                descriptor,
                1,
                getValueSerializer(value.instance),
                value.value
            )
        }
    }
}

Solution

  • That instance field looks like your class discriminator.

    In that case you can use it for built in polymophic handling without the need for writing custom serializer:

        @OptIn(ExperimentalSerializationApi::class)
        @JsonClassDiscriminator("instance")
        @Serializable
        sealed class State {
            abstract val value: Any
    
            @Serializable
            @SerialName("rgb")
            data class RGB(override val value: Int) : State()
    
            @Serializable
            @SerialName("hsv")
            data class HSV(override val value: HSValues) : State()
    
            // declare all possible types
        }