Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlinx Serialization with GraalVM Native Images #1125

Closed
kdabir opened this issue Oct 9, 2020 · 7 comments
Closed

Kotlinx Serialization with GraalVM Native Images #1125

kdabir opened this issue Oct 9, 2020 · 7 comments
Labels

Comments

@kdabir
Copy link

kdabir commented Oct 9, 2020

This may not necessarily be bug, any reference in documentation would be helpful. When compiling the example as per the blog announcement

@Serializable
data class Project(
   val name: String,
   val owner: Account,
   val group: String = "R&D"
)

@Serializable
data class Account(val userName: String)

val moonshot = Project("Moonshot", Account("Jane"))
val cleanup = Project("Cleanup", Account("Mike"), "Maintenance")

fun main() {
   val string = Json.encodeToString(listOf(moonshot, cleanup))
   println(string)
 
   val projectCollection = Json.decodeFromString<List<Project>>(string)
   println(projectCollection)
}

it works with Gradle run task (application plugin applied). However When I convert application to a graalvm native image, the compilation succeeds (and binary native image is generated) but when I run the native image, I get the following error:

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Project' is not found.
Mark the class as @Serializable or provide the serializer explicitly.
        at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:91)
        at kotlinx.serialization.internal.PlatformKt.platformSpecificSerializerNotRegistered(Platform.kt:29)
        at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:59)
        at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
        at kotlinx.serialization.SerializersKt__SerializersKt.builtinSerializerOrNull$SerializersKt__SerializersKt(Serializers.kt:79)
        at kotlinx.serialization.SerializersKt__SerializersKt.serializerByKTypeImpl$SerializersKt__SerializersKt(Serializers.kt:69)
        at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:54)
        at kotlinx.serialization.SerializersKt.serializer(Unknown Source)

To Reproduce
Just run compile the example using graalvm native image.

Expected behavior
Since I assume, kotlinx.serialization does not use reflection at runtime, the native image should have worked properly.

If there is something needed in reflection config, please update the docs.

Environment

  • Kotlin version: e.g. 1.4.10
  • Library version: e.g. 1.0.0
  • Kotlin platforms: JVM
  • Gradle version: 6.6.1
  • GraalVM: 20.2.0
@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Oct 9, 2020

Thanks!

Since I assume, kotlinx.serialization does not use reflection at runtime,

kotlinx.serialization uses reflection is exactly one place -- serializer() function to lookup a serializer.
This function is used in reified methods that do not accept serializer as a parameter.
We are going to change that soon (around Kotlin 1.4.30), but now as a temporary workaround I'd suggest to use Json.encodeToString(ListSerializer(Project.serializer()), listOf(moonshot, cleanup)) and Json.decodeFromString(ListSerializer(Project.serializer(), string) respectively.

@kdabir
Copy link
Author

kdabir commented Oct 9, 2020

Thanks @qwwdfsad for quick response. I can confirm that the workaround works with GraalVM Native Image 👍

Looking forward for kotlin 1.4.30 though :)

@kdabir kdabir closed this as completed Oct 9, 2020
@nanodeath
Copy link

Just ran into this, and the workaround is mildly annoying -- can we keep this open until

We are going to change that soon

this happens? Thanks!

@rescribet
Copy link

rescribet commented Nov 22, 2021

Just bumped into this while using ktor client, tried to add the serializer manually, but that still fails with 'serializer not found'. So I'm contextual serialization uses reflection as well.

val serializer = Json {
    serializersModule = SerializersModule {
        contextual(MySerializableClass::class, MySerializableClass.serializer())
    }
}

val client = HttpClient(CIO) {
    install(JsonFeature) {
        serializer = KotlinxSerializer(serializer)
    }
}

val data = client.get<HttpResponse>("...").receive<MySerializableClass>()

When using an explicit serializer it bumps into KotlinReflectionInternalError: Unresolved class: class java.lang.String.

val body = client.get<HttpResponse>("...").receive<String>()
val data = Json.decodeFromString(MySerializableClass.serializer(), data)

Though I guess I'll just postpone building a native image since it mostly creates unnecessary future refactor work

@qwwdfsad
Copy link
Collaborator

qwwdfsad commented May 24, 2022

It will be fixed with #1348, hopefully in Kotlin 1.8.0

@Scui1
Copy link

Scui1 commented Jun 24, 2022

kotlinx.serialization uses reflection is exactly one place -- serializer() function to lookup a serializer. This function is used in reified methods that do not accept serializer as a parameter. We are going to change that soon (around Kotlin 1.4.30), but now as a temporary workaround I'd suggest to use Json.encodeToString(ListSerializer(Project.serializer()), listOf(moonshot, cleanup)) and Json.decodeFromString(ListSerializer(Project.serializer(), string) respectively.

I just used graalvms tracing agent which generated a config for me that works without this ugly workaround. I suspect this is the part that's making it work:

{
   "name":"package.name.YourClassName",
   "fields":[{"name":"Companion"}]
},
{
   "name":"package.name.YourClassName$Companion",
   "methods":[{"name":"serializer","parameterTypes":[] }]
}

@thombergs
Copy link

I can confirm that @Scui1's suggestion above works. When you add the above JSON snippet (wrapped in an array) to, e.g. META-INF/native-image/groupid/artifactid/reflect-config.json (see GraalVM docs), you don't need to specify the serializer manually anymore.

Or, if you're using Spring, you can register your @Serializable classes like this:

class RuntimeHints : RuntimeHintsRegistrar {
    override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
        hints.reflection()
            .registerField(YourSerializable::class.java.getField("Companion"))
        hints.reflection()
            .registerMethod(YourSerializable.Companion::class.java.getMethod("serializer"), ExecutableMode.INVOKE)
       }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants