Giter Club home page Giter Club logo

Comments (19)

joffrey-bion avatar joffrey-bion commented on August 25, 2024 20

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.

ScottPierce avatar ScottPierce commented on August 25, 2024 19

I'm surprised that this hasn't been implemented yet, as this is pretty basic.

from kotlinx.serialization.

sandwwraith avatar sandwwraith commented on August 25, 2024 6

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.

natanfudge avatar natanfudge commented on August 25, 2024 5

@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.

valeriyo avatar valeriyo commented on August 25, 2024 4

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.

bolinfest avatar bolinfest commented on August 25, 2024 3

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.

natanfudge avatar natanfudge commented on August 25, 2024 3

@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.

Ayfri avatar Ayfri commented on August 25, 2024 2

Now SerialName annotation works !

from kotlinx.serialization.

felipecsl avatar felipecsl commented on August 25, 2024 1

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.

jamesonwilliams avatar jamesonwilliams commented on August 25, 2024 1

Note: In 1.5.x, StringDescriptor doesn't appear to exist anymore, but you can write serialDescriptor<String>().

from kotlinx.serialization.

natanfudge avatar natanfudge commented on August 25, 2024

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.

valeriyo avatar valeriyo commented on August 25, 2024

@natanfudge you mean in serialize()? Yes, that was sloppy. Changed it to same getEnumFieldAnnotation extension method.

from kotlinx.serialization.

natanfudge avatar natanfudge commented on August 25, 2024

@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.

sandwwraith avatar sandwwraith commented on August 25, 2024

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.

valeriyo avatar valeriyo commented on August 25, 2024

@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.

ankushg avatar ankushg commented on August 25, 2024

@natanfudge How would one go about doing this with an enum class defined by Int or Long values instead of Strings?

It looks like CommonEnumSerializer and EnumDescriptor require things to be represented as String

from kotlinx.serialization.

 avatar commented on August 25, 2024

Will @JsonNames be supported for enum class values?

from kotlinx.serialization.

sandwwraith avatar sandwwraith commented on August 25, 2024

@GlebIgnatevEmlid It should: #1458

from kotlinx.serialization.

techartist avatar techartist commented on August 25, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.