Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.37k stars 620 forks source link

Serializer forClass generates class type serializer for enums #2420

Closed abrooksv closed 2 months ago

abrooksv commented 1 year ago

Describe the bug When using the workaround to force generation of a plugin generated serializer for enums, the generated serializer always treats it as a class

To Reproduce

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
enum class Foo {
    HELLO,
    WORLD
}

@Serializable(with = Foo2Serializer::class)
enum class Foo2 {
    HELLO,
    WORLD
}

@Serializer(forClass = Foo2::class)
object Foo2Serializer : KSerializer<Foo2>

fun main() {
    println(Json.encodeToString(Foo.HELLO))
    println(Json.encodeToString(Foo2.HELLO))
}

Generates:

"HELLO"
{}

Expected behavior Serializer should act the same as if generated by @Serializable

Environment

sandwwraith commented 1 year ago

External serializer generation is an experimental feature and likely shouldn't be allowed to be used on enums.

abrooksv commented 1 year ago

That would be very sad to hear unless a new way is added to get access to a plugin generated serializer (#1169)

Right now using the generated serializer gives you support for things like case insensitive enums and @SerialName support. That is a lot of boiler plate to reproduce per enum if you want to wrap the serializer

Right now we use the plugin generated one with a fallback wrapper so that the serialization logic can be forwards compatible:

@Serializable
// Impossible to place this here to reduce copy paste from having to place at the use site: @Serializable(with = FamilySerializer::class)
enum class Family {
    @SerialName("foo")
    FOO,
    UNKNOWN
}

object FamilySerializer : KSerializer<Family> by EnumWithFallbackSerializer(Family.serializer(), Family.UNKNOWN)

class EnumWithFallbackSerializer<T : Enum<T>>(
    private val underlyingSerializer: KSerializer<T>,
    private val fallbackValue: T
) : KSerializer<T> {
    override val descriptor: SerialDescriptor = SerialDescriptor(
        "EnumWithFallbackSerializer<${underlyingSerializer.descriptor.serialName}>",
        underlyingSerializer.descriptor
    )

    override fun deserialize(decoder: Decoder): T {
        return try {
            underlyingSerializer.deserialize(decoder)
        } catch (e: SerializationException) {
            thisLogger().warn("Error deserializing message ${underlyingSerializer.descriptor.serialName}, falling back to $fallbackValue", e)
            fallbackValue
        }
    }

    override fun serialize(encoder: Encoder, value: T) {
        underlyingSerializer.serialize(encoder, value)
    }
}
sandwwraith commented 1 year ago

So it's another use-case for #1169, I see

sandwwraith commented 2 months ago

Fixed by #1169