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

Custom serializers can't be found on Kotlin/JS or Kotlin/Native if the entity implementing KSerializer is a class rather than an object

Open st4r1ight opened this issue 2 years ago • 1 comments

Describe the bug When writing a custom serializer in a project targeting Kotlin/JS or Kotlin/Native, the compiler plugin won't find the serializer if the entity that implements KSerializer<T> is a class, and will throw an error. If the entity is an object, however, the custom serializer will be found and the class will be serialized properly. When run on a NodeJS target, the serializer being a class will cause Node to exit with exit code 4, 5 or 6, although I don't know if this is something Node does in general for serialization errors. Kotlin/JVM will find the custom serializer correctly whether or not the KSerializer<T> is an object, and the JVM target also works correctly when it's part of a multiplatform project.

I will note that all my tests have been run using a unit testing framework (Kotest) because I don't know very much about multiplatform code and I haven't yet figured out how to run any code independently. I'd be happy to run some tests outside of Kotest if someone could point me in the right direction for running code on JS or Native.

To Reproduce In a multiplatform project, write a custom serializer for a class.

class Dog { ... }

class DogSerializer : KSerializer<Dog> { ... }

Mark the class as serializable with the custom serializer:

@Serializable(with = DogSerializer::class)
class Dog { ... }

Attempt to serialize using Kotlin/JS. Gradle throws the following error:

Serializer for class 'JsonTextColour' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

On Kotlin/JS explicitly declared serializer should be used for interfaces and enums without @Serializable annotation

Or, if you are targeting NodeJS, you get this rather cryptic error:

/home/user/.gradle/nodejs/node-v18.12.1-linux-x64/bin/node exited with errors: 6

However, if you change the definition of class DogSerializer to an object...

@Serializable(with = DogSerializer::class)
class Dog { ... }

object DogSerializer : KSerializer<Dog> { ... }

the compiler is able to find the serializer.

Here's the code where I first experienced the bug:

@JvmInline
@Serializable(with = JsonTextColour.Serializer::class)
value class JsonTextColour(val hex: Int) {
    companion object {
        fun fromString(colourString: String)  { ... }
    }

    /**
     * [KSerializer] for [JsonTextColour].
     * @see toString
     * @see fromString
     * */
    internal class Serializer : KSerializer<JsonTextColour> {
        override val descriptor = PrimitiveSerialDescriptor(
            serialName = "JsonText.Serializer",
            kind = PrimitiveKind.STRING
        )

        override fun serialize(encoder: Encoder, value: JsonTextColour) {
            encoder.encodeString(value.toString())
        }

        override fun deserialize(decoder: Decoder): JsonTextColour {
            val jsonString = decoder.decodeString()
            return fromString(jsonString)
        }
    }

(I know that the serializer is not using the recommended strategy for value classes; that's a planned improvement but I did test it using encodeInline() as well).

I also tested it with the custom serializer in its own file and in an outer class.

Expected behavior The serialization plugin should correctly detect and use the custom serializer, as it does in Kotlin/JVM

Environment

  • Kotlin version: [e.g. 1.3.30] 1.8.21 (also tested on 1.9.0)
  • Library version: [e.g. 0.11.0] 1.5.1
  • Kotlin platforms: [e.g. JVM, JS, Native or their combinations] Kotlin/JS and Kotlin/Native - my project includes JVM support, but as I mentioned, Kotlin/JVM is not affected by this bug.
  • Gradle version: [e.g. 4.10] 7.6.0 - I also tried on 8.2.1
  • IDE version (if bug is related to the IDE) [e.g. IntellijIDEA 2019.1, Android Studio 3.4]
  • Other relevant context [e.g. OS version, JRE version, ... ] Fedora 38. Kotlin was targeting Java 17 and NodeJS 18.12.1. Tests were run using Kotest 5.6.2., and I used Firefox 115.0.2 for testing the browser JS.

st4r1ight avatar Jul 28 '23 17:07 st4r1ight

Thanks for the detailed report! This problem likely happens because compiler plugin doesn't set associated object for JS/Native properly — it expects serializer to be object that doesn't require constructor call.

However, recommended way to define custom serializers for non-generic classes is still to declare an object Foo: KSerializer<...>.

Regarding NodeJS, I do not not why it exists with code 6 on an uncaught exception. Probably some specifics of Kotlin environment. If this bothers you, you should report this to YouTrack.

sandwwraith avatar Jul 31 '23 15:07 sandwwraith