kotlinx.serialization: Regression when serialize wrapped Any type property annotated with @Contextual

Describe the bug Kotlin version: 1.4.0 + kotlinx-serialization-core:1.0.0-RC) + @Contextual val data: Any? reports:

Exception in thread “main” kotlinx.serialization.SerializationException: Serializer for class ‘Any’ is not found.

But when (Kotlin version: 1.3.70 + kotlinx-serialization-runtime:0.20.0), @ContextualSerialization val data: Any? works fine.

To Reproduce

@Serializable
abstract class Box(
        var code: String,
        var msg: String?
){
    constructor(): this(OK, null)
    companion object{
        const val OK = "OK"
    }
}

@Serializable
//data class DataBox(@ContextualSerialization val data: Any?) : Box(OK,null)  //kotlin 1.3.70
data class DataBox(@Contextual val data: Any?) : Box(OK,null) //kotlin 1.4.0
{
    constructor(): this(null)
}

fun test3(){
    val json = Json{}
    //val str = json.stringify(DataBox.serializer(),DataBox(data = Person("Tom"))) //kotlin 1.3.70, ouput: {"code":"OK","msg":null,"data":{"name":"Tom"}}
    val str = json.encodeToString(DataBox.serializer(),DataBox(data = Person("Tom")))// kotlin 1.4.0: Execption!!! 
    println(str)
}

reports error:

Exception in thread “main” kotlinx.serialization.SerializationException: Serializer for class ‘Any’ is not found. Mark the class as @Serializable or provide the serializer explicitly. at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:127) at kotlinx.serialization.ContextualSerializer.serialize(ContextualSerializer.kt:52) at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:223) at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeNullableSerializableValue(Encoding.kt:296) at kotlinx.serialization.json.JsonEncoder$DefaultImpls.encodeNullableSerializableValue(JsonEncoder.kt) at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeNullableSerializableValue(StreamingJsonEncoder.kt:15) at kotlinx.serialization.encoding.AbstractEncoder.encodeNullableSerializableElement(AbstractEncoder.kt:94) at DataBox.write$Self(main.kt) at DataBox$$serializer.serialize(main.kt) at DataBox$$serializer.serialize(main.kt:32) at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:223) at kotlinx.serialization.json.Json.encodeToString(Json.kt:73) at MainKt.test3(main.kt:138)

Expected behavior Expect the behavior of 1.4.0 + 1.0.0-RC same as one of 1.3.70 + 0.20.0

Environment

  • Kotlin version: 1.4.0
  • Library version: 1.0.0-RC
  • Kotlin platforms: JVM
  • Gradle version: 6.3
  • IDE version: IntellijIDEA 2020.2.1 Other relevant context : MacOS 10.12.6, JDK 1.8.0_261

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 24 (2 by maintainers)

Commits related to this issue

Most upvoted comments

I’m using Map<String, Any> for the feedback purposes where attributes could be any. But in the new version, I cannot use that anymore 👎

How do I use either Contextual or Polymorphic for Map<String, Any?>?

It would be much better for me to still use Any in data classes, but if there is no other way I will have to use JsonElement.

Is it possible to write a custom serializer for Any type? I’ve tried, and it’s easy to implement a serialize method, but in deserialize method I don’t see any way to determine what’s the type of deserialized json element.

class AnySerializer : KSerializer<Any> {
    override val descriptor: SerialDescriptor = ...

    override fun deserialize(decoder: Decoder): Any {
        // No way to decode as JsonElement and check what's the type
        return decoder.decodeString()
    }

    override fun serialize(encoder: Encoder, value: Any) {
        when (value) {
            is String -> encoder.encodeString(value)
            is Int -> encoder.encodeInt(value)
            // ...
        }
    }
}

I think using T is a good idea as following. What a pity call.respond in ktor doesn’t support generic for now. (https://github.com/ktorio/ktor/issues/2013)

@Serializable
data class Box3<T>(var code: String = "OK", var msg: String? = null, val data: T? = null)
@Serializable
data class Person3(val name: String)

/**
 * Test box generic
 *
 * https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#generic-classes
 * */
fun testGeneric(){
    // {"code":"OK","msg":null,"data":{"name":"Tom"}}
    println(Json.encodeToString(Box3(data = Person3("Tom"))) )

    //{"code":"OK","msg":null,"data":[{"name":"Tom"},{"name":"Tom2"}]}
    println(Json.encodeToString(Box3(data = listOf(Person3("Tom"),Person3("Tom2")))))

    //{"code":"OK","msg":null,"data":"box primitive String"}
    println(Json.encodeToString(Box3(data = "box primitive String")))

    //{"code":"OK","msg":null,"data":1.23}
    println(Json.encodeToString(Box3(data = 1.23F)))
}

@VincentJoshuaET

Map<String, @Contextual @RawValue Any>

val json= Json {
    serializersModule = SerializersModule {
        contextual(String.serializer())
        contextual(Int.serializer())
        contextual(Double.serializer())
    }
}

In my case, Any could be String, Int or Double.

@rwsbillyang Did you tried polymorphic with generic types (String, Int, Double, …) ?

I tried just now, it does not work, same error:

@Serializable
private data class Box6(
        var code: String = "OK",
        var msg: String? = null,
        @Polymorphic var data: Any? = null
)

@Serializable
private data class Person6(val name: String)

fun testBoxAnyWithPolymorphic() {
    val format = Json {
        serializersModule = SerializersModule {
            polymorphic(Any::class) {
                subclass(Person6::class) //works ok
                //subclass(String::class) //fail: Exception in thread "main" java.lang.IllegalArgumentException: Serializer for String of kind STRING cannot be serialized polymorphically with class discriminator.
                //subclass(Long::class) //fail: Exception in thread "main" java.lang.IllegalArgumentException: Serializer for Long of kind LONG cannot be serialized polymorphically with class discriminator.
            }
        }
    }
    val box = Box6(data = Person6("Tom"))

    println("Polymorphic: ${format.encodeToString(box)}") //Polymorphic: {"code":"OK","msg":null,"data":{"type":"Person6","name":"Tom"}}

    try {
        println("Polymorphic with default Json: ${Json.encodeToString(box)}")
    } catch (e: Exception) {
        //fail box polymorphic with default Json: Class 'Person6' is not registered for polymorphic serialization in the scope of 'Any'.Mark the base class as 'sealed' or register the serializer explicitly.
        println("fail box polymorphic with default Json: ${e.message}")
    }

    val box2 = Box6(data ="Test box String")
    println("Box String with Polymorphic: ${format.encodeToString(box2)}")
    //Exception in thread "main" kotlinx.serialization.SerializationException: Class 'String' is not registered for polymorphic serialization in the scope of 'Any'. Mark the base class as 'sealed' or register the serializer explicitly.



    val data: Long = 1L
    val box3 = Box5(data = data)
    println("Box primitive Long with Polymorphic: ${format.encodeToString(box3)}")
    //Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found. Mark the class as @Serializable or provide the serializer explicitly.
}

But it works with @Contextual:

@Serializable
data class Box5(
        var code: String = "OK",
        var msg: String? = null,
        @Contextual var data: Any? = null
)

@Serializable
data class Person5(val name: String)

fun testBoxAnyWithContextual() {
    val format = Json {
        serializersModule = SerializersModule {
            contextual(Person5.serializer())
            contextual(String.serializer())//Need add this line, "Any" can be String
            contextual(Long.serializer())//Need add this line, "Any" can be Long
        }
    }

    val box = Box5(data = Person5("Tom"))
    println("Contextual: ${format.encodeToString(box)}") // ok if add: contextual(Person5.serializer())
    //Contextual: {"code":"OK","msg":null,"data":{"name":"Tom"}}

    try {
        println("Contextual with default Json: : ${Json.encodeToString(box)}")
    } catch (e: Exception) {
        //fail with default Json: Serializer for class 'Any' is not found.Mark the class as @Serializable or provide the serializer explicitly.
        println("fail with default Json: ${e.message}")
    }

    val box2 = Box5(data ="Test box String")
    println("Box primitive String with Contextual: ${format.encodeToString(box2)}")//ok if add: contextual(String.serializer())
    //Box primitive String with Contextual: {"code":"OK","msg":null,"data":"Test box String"}


    val data: Long = 1L
    val box3 = Box5(data = data)
    println("Box primitive Long with Contextual: ${format.encodeToString(box3)}")//ok if add: contextual(Long.serializer())
    //Box primitive Long with Contextual: {"code":"OK","msg":null,"data":1}
}