Comments (19)
SerialName
still just allows to use a different string than the name of the enum constant, which is still a JSON string. Are there any plans to make it easy to serialize an enum as any of its properties?
It would be nice if Kotlinx Serialization provided a generic enum serializer that would just take a "field getter" to find the property to use as serialized value. Something like:
class EnumAsValueSerializer<E : Enum<E>>(getter: (E) -> Any?): KSerializer<E>
Then it could be used this way:
class MessageTypeSerializer : EnumAsValueSerializer<MessageType>(MessageType::opcode)
@Serializable(with = MessageTypeSerializer::class)
enum class MessageType(val opcode: Int) {
TEXT(1),
BINARY(2),
}
Or, maybe even better, provide some kind of @JsonValue
annotation (like Jackson) to just mark the property to use as value:
enum class MessageType(@JsonValue val opcode: Int) {
TEXT(1),
BINARY(2),
}
from kotlinx.serialization.
I'm surprised that this hasn't been implemented yet, as this is pretty basic.
from kotlinx.serialization.
It is supported starting from Kotlin 1.3.60/kotlinx.serialization 0.14.0.
Note that enum should be annotated @Serializable
to work with @SerialName
– a special enum serializer will be generated since it is the only way to support custom names.
Regular enums are still implicitly serializable, but @SerialName/@SerialInfo
annotations won't affect them. A new IDEA inspection will help you to detect such problems.
from kotlinx.serialization.
@ankushg
If you look at the implementation of StreamingJsonOutput#encodeEnum
, you can see it will always go down as a String that way:
override fun encodeEnum(enumDescription: EnumDescriptor, ordinal: Int) {
encodeString(enumDescription.getElementName(ordinal))
}
So we need to serialize using encodeInt
with a custom serializer.
open class CommonEnumIntSerializer<T>(val serialName: String, val choices: Array<T>,val choicesNumbers: Array<Int>) :
KSerializer<T> {
override val descriptor: EnumDescriptor = EnumDescriptor(serialName, choicesNumbers.map { it.toString() }.toTypedArray())
init {
require(choicesNumbers.size == choices.size){"There must be exactly one serial number for every enum constant."}
require(choicesNumbers.distinct().size == choicesNumbers.size){"There must be no duplicates of serial numbers."}
}
final override fun serialize(encoder: Encoder, obj: T) {
val index = choices.indexOf(obj)
.also { check(it != -1) { "$obj is not a valid enum $serialName, choices are $choices" } }
encoder.encodeInt(choicesNumbers[index])
}
final override fun deserialize(decoder: Decoder): T {
val serialNumber = decoder.decodeInt()
val index = choicesNumbers.indexOf(serialNumber)
check(index != -1) {"$serialNumber is not a valid serial value of $serialName, choices are $choicesNumbers"}
check(index in choices.indices)
{ "$index is not among valid $serialName choices, choices size is ${choices.size}" }
return choices[index]
}
}
With appropriate helper methods:
interface SerialEnum {
val serialNumber: Int?
}
fun <T> Array<T>.serial() where T : SerialEnum, T : Enum<T> = this.map { it.serialNumber ?: it.ordinal }.toTypedArray()
And then we can do this:
object TestSerializer :
CommonEnumIntSerializer<TestEnum>("TestEnum", TestEnum.values(), TestEnum.values().serial())
@Serializable(with = TestSerializer::class)
enum class TestEnum(override val serialNumber: Int? = null) : SerialEnum {
Foo(123), // Will be serialized to 123
AnotherConstant // Will be serialized to 1
}
from kotlinx.serialization.
Here is a generic Enum serializer that uses @SerializedName
annotation on Enum fields.
NOTE 1: The contents of companion object
are 100% reusable, and could be moved out into common code/library.
NOTE 2: The "UNKNOWN" fallback value allows forward compatibility, so that old client doesn't crash when receiving an unknown enum value (because it was introduced later), so that it can log it and ignore - instead of crashing.
NOTE 3: The EnumSerialNameSerializer
could be modified to be more lenient (e.g. case-insensitive) if necessary.
Perhaps, there is a better way of doing this? Let me know.
package com.example
import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialDescriptor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.enumMembers
import kotlinx.serialization.internal.StringDescriptor
import kotlin.reflect.KClass
class Linter {
@Serializable(with = LintSeveritySerializer::class)
enum class LintSeverity {
@SerialName("error") ERROR,
@SerialName("warning") WARNING,
@SerialName("auto_fix") AUTOFIX,
@SerialName("advice") ADVICE,
@SerialName("disabled") DISABLED,
UNKNOWN
}
object LintSeveritySerializer : EnumSerialNameSerializer<LintSeverity>(
LintSeverity::class,
fallback = LintSeverity.UNKNOWN
)
@Serializable
private class LintMessage(
val path: String,
val line: Int? = null,
val char: Int? = null,
val severity: LintSeverity
)
companion object {
open class EnumSerialNameSerializer<E : Enum<E>>(
private val kClass: KClass<E>,
private val fallback: E
) : KSerializer<E> {
override val descriptor: SerialDescriptor = StringDescriptor
override fun serialize(encoder: Encoder, obj: E) {
encoder.encodeString(obj.getEnumFieldAnnotation<SerialName>()!!.value)
}
override fun deserialize(decoder: Decoder): E =
decoder.decodeString().let { value ->
kClass.enumMembers()
.firstOrNull { it.getEnumFieldAnnotation<SerialName>()?.value == value }
?: run {
// TODO: Log an error/warning?
fallback
}
}
}
inline fun <reified A : Annotation> Enum<*>.getEnumFieldAnnotation(): A? =
javaClass.getDeclaredField(name).getAnnotation(A::class.java)
}
}
from kotlinx.serialization.
I had a helluva time getting this to work, so hopefully to save the next person some time, here is what I believe a complete example (with fully-qualified imports!) where you want an enum value to be serialized/deserialized as lowercase:
// This annotation avoids the need for @Serializable(with=Linter.LintSeveritySerializer::class)
// elsewhere in this file, but you still need to add it in other files.
@file:UseSerializers(Linter.LintSeveritySerializer::class)
package com.example
import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialDescriptor
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.internal.StringDescriptor
class Linter {
// Cannot declare this enum private or LintSeveritySerializer
// will not compile.
enum class LintSeverity {
ERROR,
WARNING,
AUTOFIX,
ADVICE,
DISABLED
}
@Serializer(forClass = LintSeverity::class)
object LintSeveritySerializer : KSerializer<LintSeverity> {
override val descriptor: SerialDescriptor = StringDescriptor
override fun serialize(output: Encoder, obj: LintSeverity) {
output.encodeString(obj.toString().toLowerCase())
}
override fun deserialize(input: Decoder): LintSeverity {
// Admittedly, this would accept "Error" in addition to "error".
return LintSeverity.valueOf(input.decodeString().toUpperCase())
}
}
@Serializable
private class LintMessage(
val path: String,
val line: Int? = null,
val char: Int? = null,
val severity: LintSeverity
)
}
from kotlinx.serialization.
@sandwwraith That's very useful indeed.
Example usage for those looking for it:
@Serializable
class Wrapper(val test: TestEnum)
object TestSerializer : CommonEnumSerializer<TestEnum>("TestEnum", arrayOf(TestEnum.Foo), arrayOf("Bar"))
@Serializable(with = TestSerializer::class)
enum class TestEnum {
Foo // Will be serialized to "Bar"
}
Although I found doing this pattern is much more effective (use this one):
// Define this once:
interface SerialEnum {
val serialName: String?
}
fun <T> Array<T>.serial() where T : SerialEnum, T : Enum<T> = this.map { it.serialName ?: it.name }.toTypedArray()
// Then use:
@Serializable
class Wrapper(val test: TestEnum)
object TestSerializer :
CommonEnumSerializer<TestEnum>("TestEnum", TestEnum.values(), TestEnum.values().serial())
@Serializable(with = TestSerializer::class)
enum class TestEnum(override val serialName: String? = null) : SerialEnum {
Foo("Bar"), // Will be serialized to "Bar"
AnotherConstant // Will be serialized to "AnotherConstant"
}
from kotlinx.serialization.
Now SerialName
annotation works !
from kotlinx.serialization.
I found it a little hard to follow the examples above so here's a slightly different and more complete solution that uses a label
field for serialization/deserialization instead of @SerialName
:
interface LabeledEnum {
val label: String
}
object MessageTypeSerializer : MessageType.Companion.EnumLabelSerializer<MessageType>(
MessageType::class
)
@Serializable(with = EnumLabelSerializer::class)
enum class MessageType(override val label: String) : LabeledEnum {
FOO("foo"),
BAR("bar"),
BAZ("baz");
override fun toString() = label
fun toJson(configuration: JsonConfiguration = JsonConfiguration.Stable) =
Json(configuration).stringify(MessageTypeSerializer, this)
companion object {
fun fromJson(data: String, configuration: JsonConfiguration = JsonConfiguration.Stable) =
Json(configuration).parse(MessageTypeSerializer, data)
// Adapted from
// https://github.com/Kotlin/kotlinx.serialization/issues/31#issuecomment-505999158
open class EnumLabelSerializer<E>(
private val kClass: KClass<E>
) : KSerializer<E> where E : Enum<E>, E : LabeledEnum {
override val descriptor: SerialDescriptor = PrimitiveDescriptor(
"com.example.EnumLabelSerializer", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: E) {
encoder.encodeString(value.label)
}
override fun deserialize(decoder: Decoder): E =
decoder.decodeString().let { value ->
kClass.enumMembers().firstOrNull { it.label == value } ?: run {
throw IllegalStateException("Cannot find enum with label $value")
}
}
}
}
}
from kotlinx.serialization.
Note: In 1.5.x, StringDescriptor
doesn't appear to exist anymore, but you can write serialDescriptor<String>()
.
from kotlinx.serialization.
That throws a KotlinNullPointerException
for me.
This version works though:
Use this one instead is as it cross-platform and does not require reflection.
Old version:
open class EnumSerialNameSerializer<E : Enum<E>>(
private val kClass: KClass<E>
) : KSerializer<E> {
override val descriptor: SerialDescriptor = StringDescriptor
override fun serialize(encoder: Encoder, obj: E) {
val value = obj.javaClass.getField(obj.name).getAnnotation(SerialName::class.java)?.value
encoder.encodeString(value ?: obj.name)
}
override fun deserialize(decoder: Decoder): E =
decoder.decodeString().let { value ->
kClass.enumMembers()
.firstOrNull { it.getEnumFieldAnnotation<SerialName>()?.value == value }
?: run {
throw SerializationException("Could not serialize class $kClass. There are no enum members.")
}
}
}
inline fun <reified A : Annotation> Enum<*>.getEnumFieldAnnotation(): A? =
javaClass.getDeclaredField(name).getAnnotation(A::class.java)
Example usage:
@Serializable
class Wrapper(val test: Test)
object TestSerializer : EnumSerialNameSerializer<Test>(Test::class)
@Serializable(with = TestSerializer::class)
enum class Test {
@SerialName("Bar") // Will be serialized to "Bar"
Foo
}
(Update: fixed a null pointer exception that I had too)
from kotlinx.serialization.
@natanfudge you mean in serialize()
? Yes, that was sloppy. Changed it to same getEnumFieldAnnotation
extension method.
from kotlinx.serialization.
@valeriyo I think yours might crash still in this case:
@Serializable(with = TestSerializer::class)
enum class Test {
@SerialName("Bar") // Will be serialized to "Bar"
Foo,
Baz // Kotlin NPE when serializing!
}
from kotlinx.serialization.
Maybe you can also use the help of CommonEnumSerializer
. The principle is the same (create derived object), but you'll achieve multiplatform support without reflection.
from kotlinx.serialization.
@natanfudge that crash would be intentional, since I require all valid values to be annotated with @SerialName
. But the number of possibilities is large here.
@sandwwraith thanks for the tip, I didn't find CommonEnumSerializer
myself.. but since multi-platform is not needed for me, the annotation approach would be better.
from kotlinx.serialization.
@natanfudge How would one go about doing this with an enum class
defined by Int
or Long
values instead of String
s?
It looks like CommonEnumSerializer
and EnumDescriptor
require things to be represented as String
…
from kotlinx.serialization.
Will @JsonNames
be supported for enum class
values?
from kotlinx.serialization.
@GlebIgnatevEmlid It should: #1458
from kotlinx.serialization.
It is supported starting from Kotlin 1.3.60/kotlinx.serialization 0.14.0.
Note that enum should be annotated
@Serializable
to work with@SerialName
– a special enum serializer will be generated since it is the only way to support custom names.Regular enums are still implicitly serializable, but
@SerialName/@SerialInfo
annotations won't affect them. A new IDEA inspection will help you to detect such problems.
This was this for me. I did not the enum class as @serializable
from kotlinx.serialization.
Related Issues (20)
- Allow annotation meta-annotations with Serializable
- Streaming performance: kotlinx-io & Okio integrations
- kotlinx.serialization.json deserialize error HOT 1
- Quoted booleans are being allowed even with `isLenient = false` HOT 6
- very strange serialization HOT 2
- The built-in String type (JsonPrimitive/JsonLiteral) puts double quotes around the string value HOT 8
- Don't check kind if needn't type discriminator for enum and primitive HOT 5
- 2.0.20 with generatedSerializer and reference to interface: SealedClassSerializer init: subclassSerializers contains null HOT 2
- [protobuf] How to encode/decode map with null value?
- Option to serialize enums as lowercase/snake_case HOT 4
- hande single quotes when `isLenient=true` HOT 3
- Support for `serializer<@Contextual T>` HOT 3
- Not a Bug But missing step in the Documentation when working with KMP HOT 1
- "Serializer for class List is not found" on Native. HOT 2
- [FEATURE] Allow defining a sealed class as a subclass of an open polymorphic class HOT 2
- @file:UseContextualSerialization doesn't work on Kotlin 2.0.20-RC2 HOT 1
- CBOR/COSE: support sorting of keys
- Deserializing, Transitient field use default value instead of the values from subclass constructor
- Implement additional extension serializers for frequently utilized data types in web requests. HOT 1
- Provide a way to introspect `NothingSerialDescriptor` HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from kotlinx.serialization.