Skip to content

SerialName duplication for different polymorphic heirarchies causes collisons when namingStrategy is set. #3003

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

Open
FlightOfStairs opened this issue May 15, 2025 · 2 comments
Labels

Comments

@FlightOfStairs
Copy link

Describe the bug

We have parallel heirarchies of polymorphic classes. We were seeing fields in one heirarchy appearing in the serialized output of the other; only when namingStrategy is set.

To Reproduce

Kotest cases:

@file:OptIn(ExperimentalSerializationApi::class)

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonClassDiscriminator
import kotlinx.serialization.json.JsonNamingStrategy

class SerialNameCollisionTest : FunSpec({
    test("Naming strategy set - test will fail") {
        val json = Json {
            namingStrategy = JsonNamingStrategy.SnakeCase
        }

        val foo = FooBoolean(true)
        val bar = BarBoolean(true)

        json.encodeToString<Foo>(foo) shouldBe """{"type":"BOOLEAN","my_foo_value":true}"""

        // Fails at this line:
        json.encodeToString<Bar>(bar) shouldBe """{"type":"BOOLEAN","my_bar_value":true}"""
        // Expected :"{"type":"BOOLEAN","my_bar_value":true}"
        // Actual   :"{"type":"BOOLEAN","my_foo_value":true}"
    }

    test("Naming strategy not set - test will pass") {
        val json = Json { }

        val foo = FooBoolean(true)
        val bar = BarBoolean(true)

        json.encodeToString<Foo>(foo) shouldBe """{"type":"BOOLEAN","myFooValue":true}"""
        json.encodeToString<Bar>(bar) shouldBe """{"type":"BOOLEAN","myBarValue":true}"""
    }
})

@Serializable
@JsonClassDiscriminator("type")
sealed interface Foo

@Serializable
@SerialName("BOOLEAN")
data class FooBoolean(val myFooValue: Boolean) : Foo

@Serializable
@JsonClassDiscriminator("type")
sealed interface Bar

@Serializable
@SerialName("BOOLEAN")
data class BarBoolean(val myBarValue: Boolean) : Bar

In the failing test above, the first encodeToString() always provides the correct output, and the second always matches it, implying some kind of caching may be to blame.

Expected behavior

Serializing BarBoolean in the example above should never produce a field from FooBoolean. Failing that, the presense of namingStrategy should not alter this behaviour.

If neither of the above are practical, then the order in which each class is first serialized should not affect the output.

Environment

  • Kotlin version: 2.0.20
  • Library version: 1.8.1
  • Kotlin platforms: JVM
  • Gradle version: 8.6
@pdvrieze
Copy link
Contributor

@FlightOfStairs There is an assumption in the library that for a single format instance there is only one single type with the same serial Name (when you specify @SerialName on a type). The reason is that some computations are cached for speed reasons.

An additional factor is that the default equals operation for a SerialDescriptor ignores the element names: #2862 and the element names thus do not get taken into account.

@FlightOfStairs
Copy link
Author

IMO that assumption seems like a bug, but I see from linked issues it's challenging to resolve.

Could more be done to communicate that assumption to users of the library, either by reporting errors on collisions, or at least clearly stating in the docs that SerialName.value must be unique? I don't get that when reading here.

It also seems meaningful to me that the behaviour differs when namingStrategy is provided. That allows applications to be written seeing the "correct" behaviour, but then a seemingly unrelated change would cause a major impact that would be hard to root-cause.

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

No branches or pull requests

3 participants