I have the following code. Do enums get encoded by...
# serialization
z
I have the following code. Do enums get encoded by their ordinal? And if so how would I change that to encode using its serial name instead?
Copy code
@Serializable
internal enum class ChannelTab {
    @SerialName("videos")
    VIDEOS,

    @SerialName("shorts")
    SHORTS,

    @SerialName("live")
    LIVE,

    @SerialName("playlists")
    PLAYLISTS,

    @SerialName("community")
    COMMUNITY,

    @SerialName("about")
    ABOUT
}

@Serializable
internal data class ChannelParams(
    @ProtoNumber(2)
    val tab: ChannelTab
)
e
yes, the protobuf serializer encodes enums with their ordinal
untested, but I think this might work around the issue
Copy code
@Serializable
data class ChannelParams(
    @ProtoNumber(2)
    val tab: EnumWrapper<ChannelTab>
)

@Serializable(with = EnumWrapperSerializer::class)
data class EnumWrapper<E : Enum<E>>(val enum: E)

class EnumWrapperSerializer<E : Enum<E>>(private val enumSerializer: KSerializer<E>) : KSerializer<EnumWrapper<E>> {
    init {
        require(enumSerializer.descriptor.kind == SerialKind.ENUM)
    }

    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor(enumSerializer.descriptor.serialName, PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: EnumWrapper<E>) {
        encoder.encodeString(enumSerializer.descriptor.getElementName(value.enum.ordinal))
    }

    override fun deserialize(decoder: Decoder): EnumWrapper<E> {
        val ordinal = enumSerializer.descriptor.getElementIndex(decoder.decodeString())
        return EnumWrapper(enumSerializer.deserialize(object : Decoder {
            override val serializersModule: SerializersModule = EmptySerializersModule()
            override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
                throw UnsupportedOperationException()
            override fun decodeBoolean(): Boolean = throw UnsupportedOperationException()
            override fun decodeByte(): Byte = throw UnsupportedOperationException()
            override fun decodeChar(): Char = throw UnsupportedOperationException()
            override fun decodeDouble(): Double = throw UnsupportedOperationException()
            override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = ordinal
            override fun decodeFloat(): Float = throw UnsupportedOperationException()
            override fun decodeInline(descriptor: SerialDescriptor): Decoder = throw UnsupportedOperationException()
            override fun decodeInt(): Int = throw UnsupportedOperationException()
            override fun decodeLong(): Long = throw UnsupportedOperationException()
            override fun decodeNotNullMark(): Boolean = throw UnsupportedOperationException()
            override fun decodeNull(): Nothing? = throw UnsupportedOperationException()
            override fun decodeShort(): Short = throw UnsupportedOperationException()
            override fun decodeString(): String = throw UnsupportedOperationException()
        }))
    }
}
z
There's no simpler way to do it? Also what about applying the serializer to the enum class itself?
e
unless you are OK with JVM-only reflection or a custom annotation processor or compiler plugin, you have to reuse the built-in enum serializer, and I think this is the safest way to get access to it
if you are OK with reflection, then I suppose you could
Copy code
class EnumAsStringSerializer<E : Enum<E>>(enumClass: Class<E>) : KSerializer<E> {
    private val namesByValue = requireNotNull(enumClass.enumConstants)
        .associateWith { enumClass.getField(it.name).getAnnotation(SerialName::class.java)?.value ?: it.name }
    private val valuesByName = namesByValue.entries.associateBy(keySelector = { it.value }, valueTransform = { it.key })

    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(enumClass.name, PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: E) {
        encoder.encodeString(namesByValue.getValue(value))
    }

    override fun deserialize(decoder: Decoder): E = valuesByName.getValue(decoder.decodeString())
}

@Serializable(with = ChannelTab.Serializer::class)
enum class ChannelTab {
    @SerialName("videos") VIDEOS,
    // etc.
    ;
    object Serializer : KSerializer<ChannelTab> by EnumAsStringSerializer(ChannelTab::class.java)
}
(although I haven't tested that either)
z
That's unfortunate, I think for the time being the first one will work. Maybe in the future there will be a better solution I hope
e
with a bit of indirection in different places, you could
Copy code
class EnumAsStringSerializer<E : Enum<E>>(private val enumSerializer: KSerializer<E>) : KSerializer<E> {
    // basically the same as EnumWrapperSerializer previously, just without EnumWrapper
}

@Serializable(with = ChannelTab.StringSerializer::class)
enum class ChannelTab {
    ...;
    @Serializer(forClass = ChannelTab)
    object EnumSerializer

    object StringSerializer : KSerializer<ChannelTab> by EnumAsStringSerializer(EnumSerializer)
}
but it's pretty ugly either way
hmm. if you don't mind round-tripping through JSON, you could
Copy code
class EnumAsStringSerializer<E : Enum<E>>(private val enumSerializer: KSerializer<E>) : KSerializer<E> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor(enumSerializer.descriptor.serialName, PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: E) {
        encoder.encodeString(Json.encodeToJsonElement(enumSerializer, value).jsonPrimitive.content)
    }

    override fun deserialize(decoder: Decoder): E =
        Json.decodeFromJsonElement(enumSerializer, JsonPrimitive(decoder.decodeString()))
}
z
I like that approach more