Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.25k stars 615 forks source link

"Object is not abstract and does not implement abstract member 'serializer'." In native #2700

Open InsanusMokrassar opened 1 month ago

InsanusMokrassar commented 1 month ago

I am upgrading my project up to latest (currently) dependencies like Kotlin 2.0.0 and K/Serialization 1.7.0-RC and faced with the problem that compiler (or serialization plugin) do not see serializer in companion object which must have this function when compiling LinuxArm64 target

To Reproduce

git clone https://github.com/InsanusMokrassar/MicroUtils.git
cd MicroUtils
git checkout 1ca8ad9000bbf18d103d9089cb5c29fa722d8d89
./gradlew clean
./gradlew :micro_utils.language_codes:compileKotlinLinuxArm64

Expected behavior

Program is correctly compiling

Environment

sandwwraith commented 1 month ago

Minimized reproducer:

object Whatever: KSerializer<X> {
    override val descriptor: SerialDescriptor
        get() = TODO("Not yet implemented")

    override fun deserialize(decoder: Decoder): X = TODO("Not yet implemented")

    override fun serialize(encoder: Encoder, value: X): Unit = TODO("Not yet implemented")
}

@Serializable(Whatever::class)
sealed class X {
    @Serializable(Whatever::class)
    companion object : X() {}
}
sandwwraith commented 1 month ago

This code is not very clear IMO, since normally X.serializer() should return whatever X is serializable with, but in such case it would return what X.Companion is serializable with. Only the fact that these two serializers are identical saves from confusion.

InsanusMokrassar commented 1 month ago

This code is not very clear IMO, since normally X.serializer() should return whatever X is serializable with, but in such case it would return what X.Companion is serializable with. Only the fact that these two serializers are identical saves from confusion.

I agree with you, but I do not know how else provide the same serializer for different types when they actually should be serialized by the same way and serializer knows how todo it correctly

sandwwraith commented 4 weeks ago

Apparently the issue is even more deep:

@Serializable
sealed class Funny {
    @Serializable
    companion object : Funny() {}
}

@Serializable
class OuterFunny(val f: Funny)

In such a setup, OuterFunny.f will be serialized completely incorrectly — with a serializer for Funny.Companion, which always outputs {} (because it is ObjectSerializer). It looks like the plugin needs a warning or error diagnostic for that.

InsanusMokrassar commented 4 weeks ago

Apparently the issue is even more deep:

@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
sealed class Funny {
    @Serializable
    companion object : Funny() {}
}

@Serializable
class OuterFunny(val f: Funny)

In such a setup, OuterFunny.f will be serialized completely incorrectly — with a serializer for Funny.Companion, which always outputs {} (because it is ObjectSerializer). It looks like the plugin needs a warning or error diagnostic for that.

Interesting sample... To be honest, here there is problem on the type-related level, not serialization. I would name it puzzler, because of it have unpredictable behaviour in case you do not know the exact mechanism of code parser even without any annotation here.

My samples have the other case: they are using the exact serializer :)

sandwwraith commented 4 weeks ago

I think about prohibiting the usage of @Serializable on the companion object if the class is also @Serializable — because unless the serializers are strictly identical, such code will never work. Would it be hard for you to migrate from a companion object to a regular object, e.g.:

@Serializable(IetfLangSerializer::class)
sealed class Afrikaans : IetfLang() {
        override val code: String = "af"

        @Serializable(IetfLangSerializer::class)
        object Default: Afrikaans()
}

Although it may be possible to write a diagnostic in such a way that if serializers are identical, it is not reported. But I'm not sure.

InsanusMokrassar commented 4 weeks ago

I think about prohibiting the usage of @Serializable on the companion object if the class is also @Serializable — because unless the serializers are strictly identical, such code will never work. Would it be hard for you to migrate from a companion object to a regular object, e.g.:

@Serializable(IetfLangSerializer::class)
sealed class Afrikaans : IetfLang() {
        override val code: String = "af"

        @Serializable(IetfLangSerializer::class)
        object Default: Afrikaans()
}

Although it may be possible to write a diagnostic in such a way that if serializers are identical, it is not reported. But I'm not sure.

The case here is to use it like:

Afrikaans // here used Companion without region suffix
Afrikaans.NA // specific region

In case I will rewrite all on object Default it will at least break a lot of API. Besides, it will be less comfortable:

Afrikaans.Default // It is even longer than specifying the region
Afrikaans.NA // specific region