o

    Ola Gawell

    6 months ago
    Hi! I get this error when trying to do a polymorphic deserialization of a class with generic typing. I am using a custom serializer
    KSerializer
    to handle the different subclasses. Code in thread.
    java.lang.AssertionError: No such value argument slot in IrConstructorCallImpl: 0 (total=0).
    Symbol: /IntValueProviderSerializer.<init>|-5645683436151566731[0]
    We are using a multiplatform project with
    org.jetbrains.kotlinx:kotlinx-serialization-json: 1.3.2
    class Input {
        fun <T> useData(data: T): T {
            return data
        }
    }
    
    abstract class ValueProvider<T> {
        abstract fun getValue(input: Input): T
    }
    
    @kotlinx.serialization.Serializable
    class InputBasedProvider<T>(
        val data: T
    ) : ValueProvider<T>() {
        override fun getValue(input: Input): T {
            return input.useData(data)
        }
    }
    
    @kotlinx.serialization.Serializable
    data class ValueBasedProvider<T>(
        val value: T
    ) : ValueProvider<T>() {
        override fun getValue(input: Input): T = value
    }
    
    
    @Serializable
    data class DataHolder(
        @kotlinx.serialization.Serializable(with = IntValueProviderSerializer::class)
        val myData: ValueProvider<Int>,
    )
    
    class IntValueProviderSerializer : KSerializer<ValueProvider<Int>> {
        private val jsonParser = Json { isLenient = true }
    
        override fun deserialize(decoder: Decoder): ValueProvider<Int> {
            return when (val element = decoder.decodeSerializableValue(JsonElement.serializer())) {
                is JsonObject -> jsonParser.decodeFromString(InputBasedProvider.serializer(Int.serializer()), element.toString())
                else -> ValueBasedProvider(<http://element.jsonPrimitive.int|element.jsonPrimitive.int>)
            }
        }
    
        override val descriptor: SerialDescriptor = InputBasedProvider.serializer(Int.serializer()).descriptor
    
        override fun serialize(encoder: Encoder, value: ValueProvider<Int>) {
            // Skipped fow now
        }
    }
    
    class SmallTest {
    
        private val json1 = """{ "myData": 5 }""".trimIndent()
        private val json2 = """{ "myData": { "data": 9 } }""".trimIndent()
    
        @Test
        fun parseSimpleValue() {
            val dataHolder = Json.decodeFromString(DataHolder.serializer(), json1)
            assertEquals(5, dataHolder.myData.getValue(Input()))
        }
    
        @Test
        fun parseComplexValue() {
            val dataHolder = Json.decodeFromString(DataHolder.serializer(), json2)
            assertEquals(9, dataHolder.myData.getValue(Input()))
        }
    }
    a

    Adam S

    6 months ago
    it looks like you're using
    IntValueProviderSerializer
    as the serializer for a generic property -
    ValueProvider<Int>
    I think because
    IntValueProviderSerializer
    needs a constructor parameter to specify the serializer for the data type (which in your case is always Int, but the parameter still needs to be there) What happens if you add a single constructor parameter? https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers-for-a-generic-type
    I don't think the deserialize function in your
    IntValueProviderSerializer
    is right. It doesn't make sense to use an instance of Json inside a KSerializer.
    okay I think this should work for you basically, for your
    ValueBasedProvider
    class, I adapted the code in the docs which will unbox the json value
    @Serializable(with = ValueBasedProviderSerializer::class)
    data class ValueBasedProvider<T>(
      val value: T
    ) : ValueProvider<T>() {
      override fun getValue(input: Input): T = value
    }
    
    /**
     * See <https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers-for-a-generic-type>
     */
    class ValueBasedProviderSerializer<T>(
      private val dataSerializer: KSerializer<T>
    ) : KSerializer<ValueBasedProvider<T>> {
    
      override val descriptor: SerialDescriptor = dataSerializer.descriptor
    
      override fun serialize(encoder: Encoder, value: ValueBasedProvider<T>) =
        dataSerializer.serialize(encoder, value.value)
    
      override fun deserialize(decoder: Decoder) =
        ValueBasedProvider(dataSerializer.deserialize(decoder))
    }
    figuring out the serializer for
    ValueProvider
    was tricky. Normally there's
    JsonContentPolymorphicSerializer
    - but that doesn't work for generic classes. So I copied the class, and changed it so it would be like the generic serializer example it selects the deserialization based on the json content (see
    JsonContentPolymorphicSerializer
    ), and serialises based on the specific Kotlin subclass of
    ValueProvider
    (make it a
    sealed class
    and this will be nicer!)
    @Serializable
    data class DataHolder(
      @Serializable(with = ValueProviderSerializer::class)
      val myData: ValueProvider<Int>,
    )
    
    class ValueProviderSerializer<T>(
      private val dataSerializer: KSerializer<T>
    ) : KSerializer<ValueProvider<T>> {
    
      override val descriptor: SerialDescriptor = dataSerializer.descriptor
    
      override fun serialize(encoder: Encoder, value: ValueProvider<T>) {
        when (value) {
          is ValueBasedProvider -> ValueBasedProvider.serializer(dataSerializer)
            .serialize(encoder, value)
          is InputBasedProvider -> InputBasedProvider.serializer(dataSerializer)
            .serialize(encoder, value)
          else                  -> throw SerializationException("unknown ValueProvider type $value")
        }
      }
    
      override fun deserialize(decoder: Decoder): ValueProvider<T> {
    
        val jsonDecoder =
          decoder as? JsonDecoder ?: throw SerializationException("can only handle Json decoding")
    
        val element = jsonDecoder.decodeJsonElement()
    
        val serializer = when (element) {
          is JsonPrimitive -> ValueBasedProvider.serializer(dataSerializer)
          is JsonObject    -> InputBasedProvider.serializer(dataSerializer)
          is JsonArray     -> throw SerializationException("can't handle arrays")
        }
    
        return jsonDecoder.json.decodeFromJsonElement(serializer, element)
      }
    
    }
    full code
    o

    Ola Gawell

    6 months ago
    Wow, amazing! Thank you! Will try it out immediately.
    It worked perfectly! I would never have solved it by my own. Thank you!