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

Optional JsonNull cannot be deserialized (always null, never JsonNull) #2455

Closed
pschichtel opened this issue Sep 29, 2023 · 2 comments · Fixed by #2456
Closed

Optional JsonNull cannot be deserialized (always null, never JsonNull) #2455

pschichtel opened this issue Sep 29, 2023 · 2 comments · Fixed by #2456

Comments

@pschichtel
Copy link
Contributor

pschichtel commented Sep 29, 2023

I have a class like this:

@Serializable
data class Property(val name: String, val defaultValue: JsonElement? = null)

The intent is: The default value should be an optional field, however if the field is given with null in JSON, then I want it decoded as JsonNull. That way I could differentiate between "no default value given" and "default value is null".

kotlinx.serialization currently always produces null in this case if the defaultValue field is nullable. If it is not nullable, then I correctly get JsonNull.

An ugly workaround (because it is an incompatible change) would be to have a wrapper object, that has a non-nullable field:

@Serializable
data class DefaultValue(val value: JsonElement)

@Serializable
data class Property(val name: String, val defaultValue: DefaultValue? = null)

Another ugly workaround, which at least isn't a breaking change, would be to make the defaultValue non-nullable with a dummy default value that signifies that the value wasn't given in JSON:

val NoDefault = JsonPrimitive(UUID.randomUUID().toString())

@Serializable
data class Property(val name: String, val defaultValue: JsonElement = NoDefault)

To Reproduce

I guess the code to reproduce this is fairly clear:

@Serializable
data class Property(val name: String, val defaultValue: JsonElement? = null)

fun main() {
    val test1 = Json.decodeFromString<Property>(
        """
            {
              "name": "Test1",
              "defaultValue": null
            }
        """.trimIndent()
    )
    val test2 = Json.decodeFromString<Property>(
        """
            {
              "name": "Test2"
            }
        """.trimIndent()
    )
    val test3 = Json.decodeFromJsonElement<Property>(
        JsonObject(
            mapOf(
                "name" to JsonPrimitive("Test3"),
                "defaultValue" to JsonNull
            )
        )
    )
    val test4 = Json.decodeFromJsonElement<Property>(
        JsonObject(
            mapOf(
                "name" to JsonPrimitive("Test4"),
            )
        )
    )

    println("${test1.name} -> ${test1.defaultValue == JsonNull}")
    println("${test2.name} -> ${test2.defaultValue == null}")
    println("${test3.name} -> ${test3.defaultValue == JsonNull}")
    println("${test4.name} -> ${test4.defaultValue == null}")
}

This produces

Test1 -> false
Test2 -> true
Test3 -> false
Test4 -> true

Expected behavior

Test1 -> true
Test2 -> true
Test3 -> true
Test4 -> true

Environment

  • Kotlin version: 1.9.10
  • Library version: 1.6.0
  • Kotlin platforms: JVM
  • Gradle version: 8.3
@pschichtel
Copy link
Contributor Author

I decompiled the generated serializer and wrote a version of it that works:

@Serializable(PropertySerializer::class)
data class Property(val name: String, val defaultValue: JsonElement? = null)


object PropertySerializer : KSerializer<Property> {
    private val jsonElementSerializer = JsonElement.serializer()

    override val descriptor: SerialDescriptor = buildClassSerialDescriptor(Property::class.qualifiedName!!) {
        element("name", String.serializer().descriptor)
        element("defaultValue", jsonElementSerializer.descriptor)
    }

    override fun serialize(encoder: Encoder, value: Property) {
        TODO("Not yet implemented")
    }

    override fun deserialize(decoder: Decoder): Property {
        return (decoder as JsonDecoder).decodeStructure(descriptor) {
            var parsing = true
            var name: String? = null
            var defaultValue: JsonElement? = null

            while (parsing) {
                when (val index = decodeElementIndex(descriptor)) {
                    CompositeDecoder.DECODE_DONE ->
                        parsing = false
                    CompositeDecoder.UNKNOWN_NAME ->
                        throw SerializationException("unknown field!")
                    0 ->
                        name = decodeStringElement(descriptor, index)
                    1 ->
                        defaultValue = decodeSerializableElement(descriptor, index, jsonElementSerializer)
                }
            }
            if (name == null) {
                throw SerializationException("no name!")
            }
            Property(name, defaultValue)
        }
    }
}

@pschichtel
Copy link
Contributor Author

I guess the root cause is, that the JsonElement and JsonPrimitive serializers' descriptors are not nullable.

pschichtel added a commit to pschichtel/idl that referenced this issue Sep 30, 2023
this currently requires the manual implementation of RecordProperty's serializer. This can be simplified once Kotlin/kotlinx.serialization#2455 is resolved.
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

Successfully merging a pull request may close this issue.

1 participant