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

MissingFieldException when trying to deserialize enum field in sealed hierarchy #2909

Open
msalikhov opened this issue Jan 23, 2025 · 4 comments
Assignees
Labels

Comments

@msalikhov
Copy link

msalikhov commented Jan 23, 2025

Describe the bug
Exception occurred when trying to deserialize object as sealed root, if it contains field with unknown enum value.
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'status' is required for type with serial name 'org.example.Feature', but it was missing at path: $

To Reproduce

@Serializable
internal sealed class TestSealed {
    @Serializable
    @SerialName("foo")
    data class FirstSealed(@SerialName("feature") val feature: Feature) : TestSealed()
}

@Serializable
internal data class Feature(
    @SerialName("status") val status: FeatureStatus?,
)

@Serializable
internal enum class FeatureStatus {
    @SerialName("first") First,
    @SerialName("second") Second,
}

val json = """{
    "feature": {
      "status": "unknown"
    },
    "type": "foo"
}"""

fun main() {
    val parser = Json {
        coerceInputValues = true
        explicitNulls = false
        ignoreUnknownKeys = true
    }
    println(parser.decodeFromString<TestSealed>(json))
}

If try to decode as concrete impl, than all is ok parser.decodeFromString<TestSealed.FirstSealed>(json)

Also noticed that moving "type" key up - fixes the problem. Does the order of keys is important?

val json = """{
    "type": "foo",
    "feature": {
      "status": "unknown"
    }
}"""

Expected behavior
Decode success, and prints: FirstSealed(feature=Feature(status=null))

Environment

  • Kotlin version: [2.1.0]
  • Library version: [1.8.0]
  • Kotlin platforms: [JVM]
  • Gradle version: [8.10]
  • IDE version [IntelliJ IDEA 2024.3.1.1 (Community Edition)]
  • Other relevant context [MacOS, OpenJDK 23]
@sandwwraith
Copy link
Member

Yes, that indeed looks like a bug

@sandwwraith sandwwraith self-assigned this Jan 23, 2025
@msalikhov
Copy link
Author

I managed to get a little closer to the problem.
That code throws exception:

val parser = Json {
      coerceInputValues = true
      explicitNulls = false
      ignoreUnknownKeys = true
    }
    val string = """{"status": "unknown"}"""
    val json = parser.parseToJsonElement(string)
    parser.decodeFromJsonElement<Feature>(json)

@Serializable
internal data class Feature(
  @SerialName("status") val status: FeatureStatus?,
)

@Serializable
internal enum class FeatureStatus {
  @SerialName("first") First,
  @SerialName("second") Second,
}

And that is ok:

val parser = Json {
      coerceInputValues = true
      explicitNulls = false
      ignoreUnknownKeys = true
    }
    val string = """{"status": "unknown"}"""
    parser.decodeFromString<Feature>(string)

When parsing JsonElement (which fails), JsonTreeDecoder is used, and StreamingJsonDecoder is used with string

@msalikhov
Copy link
Author

msalikhov commented Jan 31, 2025

If you try to manually deserialize in such manner

val composite = decoder.beginStructure(descriptor)
    val index = composite.decodeElementIndex(descriptor)
    println("index: $index")

When parsing json element index will be -1
When parsing string index will be 0

@msalikhov
Copy link
Author

This condition inside kotlinx.serialization.json.internal.JsonTreeDecoder#decodeElementIndex seems broken?
(!configuration.coerceInputValues || !coerceInputValue(descriptor, index, name))

It looks like ! before configuration.coerceInputValues not needed. Or i missed something?

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

2 participants