Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.44k stars 623 forks source link

Misleading IndexOutOfBoundsException when decoding JsonElement as primitive #1766

Open rgmz opened 3 years ago

rgmz commented 3 years ago

Description

I encountered a confusing exception when writing JsonElement-related tests. I don't know how likely someone would be to stumble across this in the wild.

While it isn't a bug per se, the exception message could be clarified to point out the actual failure cause.

java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 0
    at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
    at java.base/java.util.Objects.checkIndex(Objects.java:372)
    at java.base/java.util.ArrayList.remove(ArrayList.java:536)
    at kotlinx.serialization.internal.TaggedDecoder.popTag(Tagged.kt:321)
    at kotlinx.serialization.internal.TaggedDecoder.decodeBoolean(Tagged.kt:223)
    at kotlinx.serialization.internal.BooleanSerializer.deserialize(Primitives.kt:86)
    at kotlinx.serialization.internal.BooleanSerializer.deserialize(Primitives.kt:82)
    at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
    at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:51)
    at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:24)
    at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:119)

To Reproduce

Consider the following (contrived) code:

fun main() {
    val jsonString = JsonPrimitive("bar")
    val jsonObject = JsonObject(mapOf("foo" to jsonString))
    val jsonArray = JsonArray(listOf(jsonObject))

    listOf(jsonString, jsonObject, jsonArray).forEach { element ->
      runCatching { Json.decodeFromJsonElement<String>(element) }
         .fold(
            onSuccess = {  println("Value is: $value") },
            onFailure = {  println("Could not decode element: ${it.message}")  }
        )
    }
}

Attempting to decode JsonObject or JsonArray as a primitive (String, Boolean, Long, etc.) will yield the confusing error mentioned above.

https://github.com/Kotlin/kotlinx.serialization/blob/43d5f7841fc744b072a636b712e194081456b5ba/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt#L320-L324

Expected behavior

Provide a meaningful exception, if possible. For example, attempting to decode a JsonObject or JsonArray as a JsonPrimitive yields a more friendly error:

kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON element, expected JsonPrimitive, had class kotlinx.serialization.json.JsonObject
JSON input: {}
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
    at kotlinx.serialization.json.JsonPrimitiveSerializer.deserialize(JsonElementSerializers.kt:76)
    at kotlinx.serialization.json.JsonPrimitiveSerializer.deserialize(JsonElementSerializers.kt:59)
    at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
    at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:51)
    at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:24)
    at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:119)

Environment

mainrs commented 2 years ago

You saved me a lot of time, thanks @rgmz :) I stumbled across this and had no clue what was going on...

sandwwraith commented 1 week ago

Still here on 1.7.3, reproducer:

@Test
fun testException() {
    val jsonObject = JsonObject(mapOf("foo" to JsonPrimitive("bar")))
    println(Json.decodeFromJsonElement<String>(jsonObject))
}