agathis
agathis

Reputation: 78

kotlinx deserialization: different types && scalar && arrays

I'm trying to deserialize a JSON like this (much more complex, but this is the essential part):

[
    {
        "field": "field1",
        "value": [1000, 2000]
    },
    {
        "field": "field2",
        "value": 1
    },
    {
        "field": "field2",
        "value":["strval2","strval3"]
    },
    {
        "field": "field4",
        "value": "strval1"
    }
]

I've tried to figure out how to use JsonContentPolymorphicSerializer in different variants but it all ends up the same: class java.util.ArrayList cannot be cast to class myorg.ConditionValue (java.util.ArrayList is in module java.base of loader 'bootstrap'; myorg.ConditionValue is in unnamed module of loader 'app')

@Serializable
sealed class ConditionValue

@Serializable(with = StringValueSerializer::class)
data class StringValue(val value: String) : ConditionValue()

@Serializable(with = StringListValueSerializer::class)
data class StringListValue(val value: List<StringValue>) : ConditionValue()

object ConditionSerializer : JsonContentPolymorphicSerializer<Any>(Any::class) {
    override fun selectDeserializer(element: JsonElement) = when (element) {
        is JsonPrimitive -> StringValueSerializer
        is JsonArray -> ListSerializer(StringValueSerializer)
        else -> StringValueSerializer
    }
}

object StringValueSerializer : KSerializer<StringValue> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StringValue")

    override fun deserialize(decoder: Decoder): StringValue {
        require(decoder is JsonDecoder)
        val element = decoder.decodeJsonElement()
        return StringValue(element.jsonPrimitive.content)
    }

    override fun serialize(encoder: Encoder, value: StringValue) {
        encoder.encodeString(value.value)
    }
}

What am I missing? And how to approach it?

Upvotes: 2

Views: 1407

Answers (1)

aSemy
aSemy

Reputation: 7109

This is indeed a difficult problem.

Probably the quickest and clearest way is to avoid getting bogged down with the 'correct' Kotlinx Serializer way and just decode the polymorphic type to a JsonElement

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement


@Serializable
data class MyData(
  val field: String,
  val value: JsonElement, // polymorphism is hard, JsonElement is easy
)

The following code produces the correct output

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

fun main() {

  val json = /*language=json*/ """
    [
        {
            "field": "field1",
            "value": [1000, 2000]
        },
        {
            "field": "field2",
            "value": 1
        },
        {
            "field": "field2",
            "value":["strval2","strval3"]
        },
        {
            "field": "field4",
            "value": "strval1"
        }
    ]
  """.trimIndent()

  val result = Json.decodeFromString<List<MyData>>(json)

  println(result)
}
[
  MyData(field=field1, value=[1000,2000]), 
  MyData(field=field2, value=1), 
  MyData(field=field2, value=["strval2","strval3"]), 
  MyData(field=field4, value="strval1")
]

Now you can manually convert MyData to a more correct instance.

  val converted = results.map { result ->

    val convertedValue: ConditionValue = when (val value = result.value) {
      is JsonPrimitive -> convertPrimitive(value)
      is JsonArray     -> convertJsonArray(value)
      else             -> error("cannot convert $value")
    }

    MyDataConverted(
      field = result.field,
      value = convertedValue
    )
  }

...


fun convertJsonArray(array: JsonArray): ConditionValueList<*> =
  TODO()

fun convertPrimitive(primitive: JsonPrimitive): ConditionValuePrimitive = 
  TODO()

As a final note, I can recommend using inline classes to represent your values. If you do want to work with Kotlinx Serialization, then they work better than creating custom serializers for primitive types.

Here's how I'd model the data in your example:

sealed interface ConditionValue

sealed interface ConditionValuePrimitive : ConditionValue

sealed interface ConditionValueCollection<T : ConditionValuePrimitive> : ConditionValue

@JvmInline
value class StringValue(val value: String) : ConditionValuePrimitive

@JvmInline
value class IntegerValue(val value: Int) : ConditionValuePrimitive

@JvmInline
value class ConditionValueList<T : ConditionValuePrimitive>(
  val value: List<T>
) : ConditionValueCollection<T>

data class MyDataConverted(
  val field: String,
  val value: ConditionValue,
)

Versions:

  • Kotlin 1.7.10
  • Kotlinx Serialization 1.3.3

Upvotes: 5

Related Questions