Search code examples
androidjsonkotlinkotlinx.serialization

Handling deserialization of a JSON field of polymorphic type (String or Int)


Our API returns a reserved_stock field that can be either a String or an Int (i.e. "158" or "5"). Since I cannot alter the API's response directly, I have to figure out a way to handle both types.

What would be the right way to do this using Kotlinx.Serialization? As of right now, I have tried declaring the field as a sealed class type and then use a custom Serializer to decode the actual value. Something along the lines of:

@Serializable(with = ReservedStockSerializer::class)
sealed class ReservedStock {
    @Serializable
    data class IntValue(val value: Int) : ReservedStock()
    @Serializable
    data class StringValue(val value: String) : ReservedStock()
}

@Serializable
object ReservedStockSerializer : KSerializer<ReservedStock> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ReservedStock", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: ReservedStock) {
        val stringRepresentation = when (value) {
            is ReservedStock.IntValue -> value.value.toString()
            is ReservedStock.StringValue -> value.value
        }
        encoder.encodeString(stringRepresentation)
    }

    override fun deserialize(decoder: Decoder): ReservedStock {
        val composite = decoder.beginStructure(descriptor)
        var intValue: Int? = null
        var stringValue: String? = null

        loop@ while (true) {
            when (val index = composite.decodeElementIndex(descriptor)) {
                CompositeDecoder.DECODE_DONE -> break@loop
                else -> {
                    when (index) {
                        0 -> intValue = composite.decodeIntElement(descriptor, index)
                        1 -> stringValue = composite.decodeStringElement(descriptor, index)
                    }
                }
            }
        }
        composite.endStructure(descriptor)

        return if (intValue != null) {
            ReservedStock.IntValue(intValue)
        } else {
            ReservedStock.StringValue(stringValue ?: "")
        }
    }
}

This does not seem to work however, since it throws a JsonException stating that it was expecting an object but instead found an Int (or String).


Solution

  • In order to make it work, i had to specify isLenient=true in the JsonConfig since our API returns unquoted values.

    I also created the following trivial JsonTransformingSerializer, like so:

    object ReservedStockSerializer : JsonTransformingSerializer<String>(String.serializer()) {
        override fun transformDeserialize(element: JsonElement): JsonElement {
            return if(element !is JsonPrimitive) JsonPrimitive("N/A") else element
        }
    }
    

    and applied it to the field in question like so:

    ...
    @Serializable(ReservedStockSerializer::class)
    @SerialName("reserved_stock")
    val reservedStock: String,
    ...