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

Json.encodeToString() includes keys with empty values when encodeDefaults is set to true #2951

Open
MartinStigen opened this issue Mar 12, 2025 · 2 comments
Labels

Comments

@MartinStigen
Copy link

Describe the bug

I am trying to create a wrapper class Optional<T> that can be used in type-safe patch requests:

@Serializable(with = OptionalSerializer::class)
interface Optional<out T> {

    @JvmInline
    value class Present<T>(val value: T) : Optional<T>

    data object NotPresent : Optional<Nothing>
}

class OptionalSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Optional<T>> {
    override val descriptor = buildClassSerialDescriptor("Optional", dataSerializer.descriptor)

    override fun deserialize(decoder: Decoder): Optional<T> = Optional.Present(dataSerializer.deserialize(decoder))

    override fun serialize(encoder: Encoder, value: Optional<T>) {
        if (value is Optional.Present) {
            dataSerializer.serialize(encoder, value.value)
        }
    }
}

To Reproduce

But when I test this:

private val json = Json { encodeDefaults = true }

@Serializable
    private data class Person(
        val name: Optional<String> = Optional.NotPresent,
        val age: Optional<Int> = Optional.NotPresent,
        val nested: Optional<Person?> = Optional.NotPresent,
    )

@Test
    fun `should not serialize absent values`() {
        // Given
        val person = Person(
            name = Optional.NotPresent,
            age = Optional.NotPresent,
            nested = Optional.NotPresent,
        )

        // When
        val actualJsonString = json.encodeToString(person)

        // Then
        actualJsonString shouldBe "{}"
    }

I get: Expected :"{}", Actual :"{"name":,"age":,"nested":}"

Expected behavior

The behaviour I would expect would be for the string to be just {}.
It works when I set encodeDefault = false, but for other serialization cases I need it to be true.

Environment

  • Kotlin version: 2.1.10
  • Library version: 1.6.0
  • Kotlin platforms: JVM
  • Gradle version: 8.6
@pdvrieze
Copy link
Contributor

@MartinStigen Unfortunately it is not valid for (custom) serializers to serialize nothing. In the case of Json you're lucky the format actually produces something, but fundamentally the issue is with the serializer. If a value should be omitted this needs to be done before the serializer is called for the value (as the key/element name is separate and already written by that time). Have a look at how TagEncoder.encodeSerializableElement works through using TagEncoder.encodeElement to determine whether the element should be writen.

What you can do is have it serialize null and then using the settings omit the writing of null attributes. Otherwise you will need a custom serializer on the owner of the optionals, or use the @EncodeDefault annotation on the property to override the format. For the inline value you can then make this work regardless.

@sandwwraith
Copy link
Member

The whole framework is structured in a way that only the outer class (Person) serializer can decide whether to include its properties (name, age, ...) or not. You cannot decide this with the serializer for property type (OptionalSerializer). So if you want some custom logic to include or exclude properties, you have to write a PersonSerializer. However, in your case, it will be much easier to use the @EncodeDefault(NEVER) annotation if you don't want to change the state of the global flag.

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