kotlinx.serialization
kotlinx.serialization copied to clipboard
Multiplatform: `ClassCastException` acquiring `serializer()`
Describe the bug
I have a Kotlin Multiplatform class as follows:
Schema.kt:
@MetaSerializable
annotation class Schema
Document.kt:
@Schema
expect class Document {
val id: String?
val isCheckedOut: Boolean
// ... many other properties ... //
}
The enclosed error surfaces when calling Document.serializer().
Error:
Caused by: java.lang.ExceptionInInitializerError: null
(... application code ...)
at io.micronaut.context.DefaultBeanContext.resolveByBeanFactory(DefaultBeanContext.java:2354)
... 29 common frames omitted
Caused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Boolean (java.lang.String and java.lang.Boolean are in module java.base of loader 'bootstrap')
at (app.package.path).Document$$serializer.<clinit>(Document.kt:15)
... 37 common frames omitted
To Reproduce I can extract a code snippet upon request -- this is a private codebase, but the snippet could be made generic to Kotlin Multiplatform.
Expected behavior
The property should simply become nullable. In any case, an exception surfacing at invocation time of .serializer() seems unintended and hard to handle.
Environment
- Kotlin version:
1.7.20-RC - Library version:
1.4.0 - Kotlin platforms:
Multiplatform/JVM - Gradle version:
7.5 - IDE version (if bug is related to the IDE): N/A
- Other relevant context: macOS 12.6 (Monterey), GraalVM 22.2
I've updated this to reflect that I'm not sure a Boolean / Boolean? field has anything to do with the error. It seems to happen for some compilations and not others.
Hi, thanks for the report! Unfortunately, I wasn't able to reproduce the issue on a sample project. Maybe it's caused by the fact that different files in the project were compiled with different compiler versions? If you can reproduce the issue consistently, please send an example project.
@sandwwraith unlikely RE/compiler versions, but I agree it is almost impossible to reproduce. I'll re open if I can nail it down, thank you for the quick response.
at the moment this is precluding us from using KotlinX serialization at all.
do you have any tips about inspecting the Kotlin bytecode produced by the Serializaton plug-in?
If you want to examine bytecode, your best bet is to open compiled classfile and decompile it to Java (using e.g. built-in IntelliJ decompiler)
@sandwwraith this has popped up again, and is turning out to be a bit of a show stopper for us. it does seem to be related to MPP - i don't get this exception for any objects or classes other than expect/actual definitions.
meta-serializable:
@OptIn(ExperimentalSerializationApi::class)
@MetaSerializable
@MustBeDocumented
@Target(AnnotationTarget.CLASS)
annotation class Schema(
val name: String = "",
val plural: String = "",
val builtin: Boolean = false,
val ns: Namespace = Namespace.BASE,
)
expect definition:
@file:OptIn(ExperimentalSerializationApi::class)
package com.dyme.pxm.model.org
import ...Schema
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.protobuf.ProtoNumber
@Schema("TenantDirectory", "TenantDirectories", builtin = true, ns = Namespace.CLOUD)
expect class TenantDirectory {
@ProtoNumber(1) @SerialName("orgId") val orgId: String
@ProtoNumber(2) @SerialName("live") val live: Boolean
@ProtoNumber(3) @SerialName("directoryId") val directoryId: String
@ProtoNumber(4) @SerialName("enabled") val enabled: Boolean
@ProtoNumber(5) @SerialName("lastSyncAt") val lastSyncAt: Instant
@ProtoNumber(6) @SerialName("updatedAt") val updatedAt: Instant
@ProtoNumber(7) @SerialName("createdAt") val createdAt: Instant
}
actual definition (JVM):
@Schema("TenantDirectory", "TenantDirectories", builtin = true, ns = Namespace.CLOUD)
actual data class TenantDirectory(
actual val orgId: String,
actual val live: Boolean,
actual val directoryId: String,
actual val enabled: Boolean,
actual val lastSyncAt: Instant,
actual val updatedAt: Instant,
actual val createdAt: Instant,
)
callsite:
object ProtoGenerator {
val allModels = listOf(
// ...
TenantDirectory::class,
)
private val descriptors = allModels.map {
it.serializer().descriptor
}
@JvmStatic fun main(args: Array<String>) {
val schemas = ProtoBufSchemaGenerator.generateSchemaText(
descriptors
)
println(schemas)
}
}
exception (from Gradle):
> Task :commons:run FAILED
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.reflect.InvocationTargetException
at kotlinx.serialization.internal.PlatformKt.invokeSerializerOnCompanion(Platform.kt:114)
at kotlinx.serialization.internal.PlatformKt.constructSerializerForGivenTypeArgs(Platform.kt:50)
at kotlinx.serialization.internal.PlatformKt.constructSerializerForGivenTypeArgs(Platform.kt:39)
at kotlinx.serialization.internal.PlatformKt.compiledSerializerImpl(Platform.kt:23)
at kotlinx.serialization.SerializersKt__SerializersKt.serializerOrNull(Serializers.kt:180)
at kotlinx.serialization.SerializersKt.serializerOrNull(Unknown Source)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:157)
at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
at ...ProtoGenerator.<clinit>(ProtoGenerator.kt:24)
Caused by: java.lang.ExceptionInInitializerError
at ...TenantDirectory$Companion.serializer(TenantDirectory.kt:44)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at kotlinx.serialization.internal.PlatformKt.invokeSerializerOnCompanion(Platform.kt:109)
... 8 more
Caused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Boolean (java.lang.String and java.lang.Boolean are in module java.base of loader 'bootstrap')
at ...TenantDirectory$$serializer.<clinit>(TenantDirectory.kt:11)
... 14 more
when i open up the compiled bytecode, all i get from IDEA is:
@...Schema public final data class TenantDirectory public constructor(orgId: kotlin.String, live: kotlin.Boolean, directoryId: kotlin.String, enabled: kotlin.Boolean, lastSyncAt: kotlinx.datetime.Instant, updatedAt: kotlinx.datetime.Instant, createdAt: kotlinx.datetime.Instant) {
@kotlin.Deprecated public constructor(seen1: kotlin.Int, orgId: kotlin.String?, live: kotlin.Boolean, directoryId: kotlin.String?, enabled: kotlin.Boolean, lastSyncAt: kotlinx.datetime.Instant?, updatedAt: kotlinx.datetime.Instant?, createdAt: kotlinx.datetime.Instant?, serializationConstructorMarker: kotlinx.serialization.internal.SerializationConstructorMarker?) { /* compiled code */ }
// ...
}
i.e., i just get /* compiled code */ for the lines where the error purportedly takes place. do you have any tips to illuminate those code blocks, or otherwise get you more info for diagnosis? thank you for any help you can offer of course
@sandwwraith okay, i've tracked it down a bit more. after removing builtin from the @Schema annotation, the error cleared up:
@OptIn(ExperimentalSerializationApi::class)
@MetaSerializable
@MustBeDocumented
@Target(AnnotationTarget.CLASS)
annotation class Schema(
val name: String = "",
val plural: String = "",
val ns: Namespace = Namespace.BASE,
)
then, replicating the same annotation and playing around with defaults, i was able to trigger the error again. i realize default values are banned in @Serializable annotations, but that exception only surfaces in some cases, seemingly, and i had missed it one way or another in my workflow; making it past that exception, the serializer's class initializer breaks.
to simulate:
@OptIn(ExperimentalSerializationApi::class)
@MetaSerializable
@MustBeDocumented
@Target(AnnotationTarget.CLASS)
annotation class SchemaExample(
val name: String = "",
val plural: String = "",
val builtin: Boolean = false,
)
/** */
@SchemaExample(builtin = true)
expect class Tag {
val id: String?
val name: String?
}
if i understand correctly, now, this should get:
IllegalArgumentException: Can't use arguments with defaults for serializable annotations yet
but instead gets, at runtime, the exception attached. in this sample, that's:
Caused by: java.lang.ExceptionInInitializerError
at Tag$Companion.serializer(Tag.kt:7)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at kotlinx.serialization.internal.PlatformKt.invokeSerializerOnCompanion(Platform.kt:109)
Caused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class ...Namespace (java.lang.String is in module java.base of loader 'bootstrap'; com.dyme.pxm.meta.Namespace is in unnamed module of loader 'app')
That's an interesting result. Maybe it is due to the fact that annotation's parameter defaults can be analyzed from a different module? (Given the expect/actual situation).
To watch decompiled code, you need first to open Kotlin class file (and see /** compiled code*/) then invoke action 'decompile to Java' (or select Tools - Kotlin - Decompile to Java). Unfortunately classes can't be decompiled to Kotlin now,. so you need to view Java code