kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

SerializationException: Class 'ArrayList' is not registered for polymorphic serialization in the scope of 'Any'.

Open jannehof opened this issue 4 years ago • 10 comments

Describe the bug

Following code work on serialization pre-release artifact org.jetbrains.kotlinx:kotlinx-serialization-runtime. When using same code with 1.0.0-RC2 or later with artifact org.jetbrains.kotlinx:kotlinx-serialization-json

I get runtime exception

SerializationException: Class 'ArrayList' is not registered for polymorphic serialization in the scope of 'Any'.

I've tried to pull out the essential parts from a larger project.

#1252 may be related. I'm not sure if this is a duplicate of different use case, or a separate issue.

To Reproduce

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*


interface RpcSerializer<ReqT, ResT> {

    fun serializeRequest(rpcRequest: RpcRequest): ReqT
    fun serializeResponse(rpcResponse: RpcResponse): ResT

    fun deserializeRequest(request: ReqT): RpcRequest
    fun deserializeResponse(response: ResT): RpcResponse

}

class JsonRpcSerializer(serializerModule: SerializersModule) : RpcSerializer<String, String> {

    private val serializer = Json {
        useArrayPolymorphism = true
        encodeDefaults = true
        serializersModule = serializerModule
    }

    override fun serializeRequest(rpcRequest: RpcRequest): String {
        return serializer.encodeToString(rpcRequest)
    }

    override fun serializeResponse(rpcResponse: RpcResponse): String {
        return serializer.encodeToString(rpcResponse)
    }

    override fun deserializeRequest(request: String): RpcRequest {
        return serializer.decodeFromString(request)
    }

    override fun deserializeResponse(response: String): RpcResponse {
        return serializer.decodeFromString(response)
    }

}

class RpcSerializerImpl constructor(

    private val actualSerializer: RpcSerializer<String, String> = JsonRpcSerializer(
        module
    )
) : RpcSerializer<String, String> {

    override fun serializeRequest(rpcRequest: RpcRequest) = actualSerializer.serializeRequest(rpcRequest)
    override fun serializeResponse(rpcResponse: RpcResponse) = actualSerializer.serializeResponse(rpcResponse)
    override fun deserializeRequest(request: String) = actualSerializer.deserializeRequest(request)
    override fun deserializeResponse(response: String) = actualSerializer.deserializeResponse(response)
}

@Serializable
data class Identifier(val name: String, val description: String)

@Serializable
data class Point(val x: Double, val y: Double, val id: Identifier)

@Serializable
data class RpcIdentifier(
    val serviceName: String,
    val name: String,
    val parameters: List<String>,
    val returnType: String
)

@Serializable
data class RpcRequest(
    val id: Long,
    val functionSignature: RpcIdentifier,
    val arguments: List<RpcData>
)


@Serializable
class RpcSynchronousResponse(
    override val id: Long,
    @Polymorphic
    val data: RpcData
): RpcResponse() {
    override fun toString(): String {
        return "RpcSynchronousResponse[$id]($data)"
    }
}


val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(RpcSynchronousResponse::class)
        subclass(RpcData::class)
        subclass(Point::class)

    }
    polymorphic(RpcData::class) {
        subclass(RpcData::class)
    }

    polymorphic(RpcResponse::class) {
        subclass(RpcSynchronousResponse::class)
    }
}

val format = Json { serializersModule = module }

@Serializable
data class RpcData(
    @Polymorphic
    val value: Any?
)


@Serializable
abstract class RpcResponse {
    abstract val id: Long
}

@Serializable
class RpcExceptionResponse(
    override val id: Long,
    val exception: String
): RpcResponse() {
    override fun toString(): String {
        return "RpcExceptionResponse[$id]($exception)"
    }
}



fun main() {

    val serializer = RpcSerializerImpl()

    val point1 = Point(1.0,1.0,Identifier("one","111"))
    val point2 = Point(2.0,-1.0,Identifier("two","222"))
    val data = RpcData(listOf(point1,point2))

    val responseToSerialize = RpcSynchronousResponse(42L, data)
    println("responseToSerialize: $responseToSerialize")

    val jsonString = serializer.serializeResponse(responseToSerialize)
    println("jsonString $jsonString")

    val backAgain = serializer.deserializeResponse(jsonString)
    println("backAgain: $backAgain")
}

Expected behavior

Same result as when using pre-release org.jetbrains.kotlinx:kotlinx-serialization-runtime version 1.0-M1-1.4.0-rc, printout as follows:

responseToSerialize: RpcSynchronousResponse[42](RpcData(value=[Point(x=1.0, y=1.0, id=Identifier(name=one, description=111)), Point(x=2.0, y=-1.0, id=Identifier(name=two, description=222))])) jsonString ["RpcSynchronousResponse",{"id":42,"data":["RpcData",{"value":["kotlin.collections.ArrayList",[["Point",{"x":1.0,"y":1.0,"id":{"name":"one","description":"111"}}],["Point",{"x":2.0,"y":-1.0,"id":{"name":"two","description":"222"}}]]]}]}] backAgain: RpcSynchronousResponse[42](RpcData(value=[Point(x=1.0, y=1.0, id=Identifier(name=one, description=111)), Point(x=2.0, y=-1.0, id=Identifier(name=two, description=222))]))

Environment

  • Kotlin version: 1.4.30
  • Library version: 1.1.0-RC
  • Kotlin platforms: JVM,
  • Gradle version: 6.6.1
  • IDE version (if bug is related to the IDE) [e.g. IntellijIDEA 2020.3.2
  • Other relevant context [e.g. OS version, JRE version, ... ] Java 1.8

jannehof avatar Feb 16 '21 14:02 jannehof

https://youtrack.jetbrains.com/issue/KT-44953

ivakub avatar Feb 17 '21 12:02 ivakub

I managed to find different scenario for this error (Kotlin 1.5.10, kotlinx.serialization 1.2.1, JDK 11):

Code:

class ArrayListBug {
    val json = Json {
        useArrayPolymorphism = true
        serializersModule = SerializersModule {
            polymorphic(Any::class) {
                subclass(String::class, String.serializer())
                subclass(Boolean::class, Boolean.serializer())
                subclass(List::class, ListSerializer(PolymorphicSerializer(Any::class).nullable))
            }
        }
    }

    @Test
    fun b() {
        val serializer = PolymorphicSerializer(Any::class)

        println(json.encodeToString(serializer, "abc")) // prints ["kotlin.String","abc"]
        println(json.encodeToString(serializer, true)) // prints ["kotlin.Boolean",true]
        try {
            println(json.encodeToString(serializer, listOf<Any>())) // fails with error
        } catch (e: Exception) {
            // JVM, Native and JS fails with: Class 'EmptyList' is not registered for polymorphic serialization in the scope of 'Any'.
            e.printStackTrace()
        }
        try {
            println(json.encodeToString(serializer, listOf<Any>("A"))) // fails with error
        } catch (e: Exception) {
            // JVM fails with: Class 'SingletonList' is not registered for polymorphic serialization in the scope of 'Any'.
            // Native fails with: Class 'class <anonymous>' is not registered for polymorphic serialization in the scope of 'Any'.
            // JS fails with: Class 'ArrayList' is not registered for polymorphic serialization in the scope of 'Any'.
            e.printStackTrace()
        }
        try {
            println(json.encodeToString(serializer, listOf<Any>("A", true))) // fails with error
        } catch (e: Exception) {
            // JVM fails with: Class 'ArrayList' is not registered for polymorphic serialization in the scope of 'Any'.
            // Native fails with: Class 'class <anonymous>' is not registered for polymorphic serialization in the scope of 'Any'.
            // JS fails with: Class 'ArrayList' is not registered for polymorphic serialization in the scope of 'Any'.
            e.printStackTrace()
        }
    }
}

I think root cause is that List have different implementation classes in runtime, therefore it is impossible to register their serializer to do polymorphic serialization correctly.

Virelion avatar Jun 03 '21 15:06 Virelion

I think default polymorphic serializers for collections were removed in 1.0.0 because it is kinda a weird request to serialize lists polymorphically (because it has many different implementation classes, yes). You may workaround this by writing a custom serializer for your @Polymorphic Any property

sandwwraith avatar Jun 04 '21 15:06 sandwwraith

I think default polymorphic serializers for collections were removed in 1.0.0 because it is kinda a weird request to serialize lists polymorphically (because it has many different implementation classes, yes). You may workaround this by writing a custom serializer for your @Polymorphic Any property

I don't get how this custom serializer shall be written. It only need to work on jvm. For the jvm it is so far working to use the old artifact on newer kotlin versions, so that is my current work around. @sandwwraith any hint how such serializer may look given the "to the point" ArrayListBug class that @Virelion provided.

jannehof avatar Feb 16 '22 10:02 jannehof

@jannehof Just copy subclass(List::class, ListSerializer(PolymorphicSerializer(Any::class).nullable)) and replace List::class with ArrayList::class

sandwwraith avatar Feb 21 '22 14:02 sandwwraith

Sounds easy, but I don't get how subclass(ArrayList::class, ListSerializer(PolymorphicSerializer(Any::class).nullable)) don't compile Type mismatch: inferred type is List<Any?> but ArrayList<*> was expected

jannehof avatar Feb 22 '22 18:02 jannehof

@jannehof this worked for me by adding a simple cast fun. My code: subClass(ArrayList::class, ListSerializer(PolymorphicSerializer(Any::class)).cast()) the extension fun: @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") private inline fun <T> KSerializer<List<Any>>.cast(): KSerializer<T> { return this as KSerializer<T> }

My use case: I needed to serializer a list which was either a string or a customDto. If u you need any more info for my code let me know. @sandwwraith i think it's ok to close this issue as your example works.

GeorgePap-719 avatar Apr 11 '22 09:04 GeorgePap-719

The input from @GeorgePap-719 helped and working for ArrayList, now I'm stuck figuring out how to add serializer in polymorphic context for 'SingletonList'

Update: I'll try make my make an ArrayList of it before trying serialize it. (Arraylist(list))

jannehof avatar Dec 19 '22 11:12 jannehof

Im guessing you are using this factory fun:

public fun <T> listOf(element: T): List<T> = java.util.Collections.singletonList(element)

Try to use this fun from stdlib which is not returning singletonList :

public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()

GeorgePap-719 avatar Dec 19 '22 14:12 GeorgePap-719

This problem is very generalized and quite easy to run into when doing GADTs. It happens whenever you fully-type a partially-typed GADT child-element (using a collection-type **) and then widen it to the GADT-root. This can happen in a lot of different situations. For example, here's a function that takes a generic GADT value and returns it (presumably in real-world cases it would do something inside).

** Or any type that takes at least one generic parameter!

  @Serializable
  sealed interface Root<A, B>
  @Serializable
  data class PartiallyTyped<A>(val value: A): Root<A, String>

  fun <A> passValue(root: Root<A, String>): Root<A, String> = root

  fun gadt() {
    val value = passValue(PartiallyTyped(listOf(1,2,3)))
    println(Json.encodeToString(value))
    // ========= Boom! =========
    // Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for subclass 'ArrayList' is not found in the polymorphic scope of 'Any'.
  }

Or just imagine a GADT that accepts a child field:

  @Serializable
  sealed interface Root<A, B>
  @Serializable
  data class Parent<A, B>(val child: Root<A, B>): Root<A, B>
  @Serializable
  data class PartiallyTyped<A>(val value: A): Root<A, String>

  fun gadt() {
    val value = Parent(PartiallyTyped(listOf(1,2,3)))
    println(Json.encodeToString(value))
  }
  // ========= Boom! =========
  // Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for subclass 'ArrayList' is not found in the polymorphic scope of 'Any'.

Bottom line, kotlinx-serialization is highly problematic for anyone trying to use it with GADTs where you inherently want to have partially-typed elements.

Also note, despite the fact that I used Json.encodeToString, this issue has nothing to do with the Json encoder specifically. In fact, I discovered it in a completely different encoder.

deusaquilus avatar Feb 01 '24 04:02 deusaquilus