SerializationException: Class 'ArrayList' is not registered for polymorphic serialization in the scope of 'Any'.
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
https://youtrack.jetbrains.com/issue/KT-44953
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.
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 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 Anyproperty
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 Just copy subclass(List::class, ListSerializer(PolymorphicSerializer(Any::class).nullable)) and replace List::class with ArrayList::class
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 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.
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))
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()
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.