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

26 Views Asked by At

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

1

There are 1 best solutions below

0
Stelios Papamichail On BEST ANSWER

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,
...