Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serialName doesn't actually return @SerialName as per docs #2956

Open
chrisjenx opened this issue Mar 17, 2025 · 10 comments
Open

serialName doesn't actually return @SerialName as per docs #2956

chrisjenx opened this issue Mar 17, 2025 · 10 comments

Comments

@chrisjenx
Copy link

Describe the bug

descriptor.serialName as per the docs should return the overridden @SerialName for that field/type, but doesn't seem to respect that.

For generated and default serializers, the serial name is equal to the corresponding class's fully qualified name or, if overridden, SerialName

To Reproduce

@Serializable
data class TestClass {
 @SerialName("notField")
 val field: String,
}


assertTrue(TestClass::serializer().getElementDescriptor(0).serialName == "notField") // fails

Unless I'm missing something, it seems impossible to get the annotated @SerialName from the descriptor at runtime.

(Semi related, it's also impossible to find the correct element descriptor at runtime as you can't reverse lookup by elementName)

Expected behavior

Not sure if serialName is returning the wrong thing? But ideally I should return notField as per the docs for val field.
If that is expected behavior, then the docs need updating. And we need a method to actually get that @SerialName.

Environment

  • Kotlin version: [e.g. 1.3.30] 2.1.10
  • Library version: [e.g. 0.11.0] 1.8.0
  • Kotlin platforms: [e.g. JVM, JS, Native or their combinations] All
  • Gradle version: [e.g. 4.10] 8.13 (but any version)

I have more examples I can share, Sqkon uses this heavily

@sandwwraith
Copy link
Member

SerialDescriptor.serialName returns a name of serializable class/serializer. If you want to get the name of property/field, use getElementName — in your case, TestClass.serializer().descriptor.getElementName(0).

@chrisjenx
Copy link
Author

chrisjenx commented Mar 17, 2025

That doesn't work either, see test:

enum class TestEnum {
    FIRST,
    SECOND,

    @SerialName("unknown")
    LAST;
}


    @OptIn(InternalSerializationApi::class)
    @Test
    fun enumMissingSerialName() {

        assertEquals(
            TestEnum::class.serializer().descriptor.getElementName(TestEnum.LAST.ordinal),
            "unknown"
        )

    }
Expected :unknown
Actual   :LAST

@chrisjenx
Copy link
Author

Lets say you want to use getElementName() which should work on most fields. But you only have the propertyName, for example.

@Serializable
data class TestClass(
 val firstString: String = "",
 @SerialName("diffName")
 val propName: String = ""
)

at runtime i only have access to the KProperty so I can derive the Reciever type and propertyName. KClass<TestClass> and propName respectively.

You would do something like:

val index = receiverDescriptor.getElementIndex(it.propertyName)
val name = receiverDescriptor.getElementName(index)

Which is impossible as the parent descriptor only holds the @SerialName, so you get invalid index (out of bounds) so you can't reverse lookup.

You could also try storing the value descriptor:

val index = it.receiverDescriptor.elementDescriptors.indexOf(it.valueDescriptor)
val name = it.receiverDescriptor.getElementName(index)

That will also fail because child descriptors are NOT unique by equality, two string descriptors are the same (understandable)

Which comes back to, we either need to be able to pull SerialName from the ValueDescriptor or store the propertyName on the receiverDescriotor so can cross index descriptors.

With reflection we could find the index of the property in it's parent, but needs to work on multi platform code etc...

@chrisjenx
Copy link
Author

@pdvrieze
Copy link
Contributor

That doesn't work either, see test:

enum class TestEnum {
    FIRST,
    SECOND,

    @SerialName("unknown")
    LAST;
}


    @OptIn(InternalSerializationApi::class)
    @Test
    fun enumMissingSerialName() {

        assertEquals(
            TestEnum::class.serializer().descriptor.getElementName(TestEnum.LAST.ordinal),
            "unknown"
        )

    }
Expected :unknown
Actual   :LAST

If you want to make @SerialName to work on an enum you will have to annotate the enum as @Serializable

@pdvrieze
Copy link
Contributor

As a separate point, there is no reliable way to map properties (or property names) to serial elements/the serial descriptor. You could investigate and mirror the structure of the generated serializers (and library implemented serializers). However, with custom serializers there is no need for there to even be a property/element match at all.

@chrisjenx
Copy link
Author

If you want to make @SerialName to work on an enum you will have to annotate the enum as @Serializable

Thats inconsistant behaviour, if you serialize the object to json output will use the @SerialName regardless of having @Serializable on it. But using the descriptor directly won't.

Part of the issue is that the JSON path doesn't match because the serialized enum uses the SerialName, but the descriptor doesn't give it to us... why the discrepancy ?

@chrisjenx
Copy link
Author

As a separate point, there is no reliable way to map properties (or property names) to serial elements/the serial descriptor. You could investigate and mirror the structure of the generated serializers (and library implemented serializers). However, with custom serializers there is no need for there to even be a property/element match at all.

Not sure what you mean here? We use descriptors at runtime to work out json paths based on KProperties and KClass's. The only info I have at runtime is the KProperty.name and the descriptor for the parent class and the field.

If you are iterating across fields it's fine you don't need that, but as the SerialName gets erased and the propertyName is not kept it's really hard to find the serialName for a property name.

I did look at internal implementation, unless I missed something, not sure what would help me here?

@pdvrieze
Copy link
Contributor

Not sure what you mean here? We use descriptors at runtime to work out json paths based on KProperties and KClass's. The only info I have at runtime is the KProperty.name and the descriptor for the parent class and the field.

If you are iterating across fields it's fine you don't need that, but as the SerialName gets erased and the propertyName is not kept it's really hard to find the serialName for a property name.

As you say in the second paragraph this information is not available. The other point I was making is that even this poor availability is not actually applicable to custom serializers that could make up elements (from a descriptor perspective) from whole cloth or perform complex transformations. (this means that the mapping is not possible at all)

I did look at internal implementation, unless I missed something, not sure what would help me here?

On some platforms it may be possible to use annotations with runtime retention that also have a @SerialInfo annotation, and can thus be mapped between both contexts: reflection/properties and serialization/elementName.

My understanding is that you want to be able to use typesafe accessors to query into an object database. I would say that the best way to do this is a separate (or extended) compiler plugin (maybe using ksp) that generates such accessors. These accessors might even be derived from the serial descriptor (even custom ones), but it would certainly complicate matters.

@chrisjenx
Copy link
Author

Yeah, I think might have to go down the route of kotlin compiler (don't want to rely on KSP as it would be super small compiler plugin)

If you could loop back on why json.serializeToString() will handle enum SerialName correctly but the descriptor doesn't would be interested to know. That does feel like a discrepancy to me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants