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

sealed interface with value class subtypes + JsonContentPolymorphicSerializer: Index -1 out of bounds for length 0 #2159

Closed
aSemy opened this issue Jan 17, 2023 · 1 comment
Assignees

Comments

@aSemy
Copy link
Contributor

aSemy commented Jan 17, 2023

Describe the bug

Requirement is to decode polymorphic JSON, where the JSON can be one of

  • String
  • Set<String>
  • Map<String, String>

To try to solve this I created a sealed interface, AnyValue, with 3 value class subtypes. To polymorphically decode I defined a JsonContentPolymorphicSerializer.

To Reproduce

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.*

fun main() {

  // plain string -> works
  val anyValueString: AnyValue = Json.decodeFromString(""" "I'm a string" """)
  println(anyValueString)


  val complexJson = """ {"id": "1", "name": "object"} """

  // works
  val anyValueComplex1: AnyValue.Complex = Json.decodeFromString(complexJson)
  println(anyValueComplex1)

  try {
    // fails
    val anyValueComplex2: AnyValue = Json.decodeFromString(complexJson)
    println(anyValueComplex2)
  } catch (e: Exception) {
    println("⚠️ anyValueComplex2 failed")
  }

  val multiJson = """["list", "of", "strings"]"""

  // works
  val anyValueList1: AnyValue.Multi = Json.decodeFromString(multiJson)
  println(anyValueList1)

  try {
    // fails
    val anyValueList2: AnyValue = Json.decodeFromString(multiJson)
    println(anyValueList2)
  } catch (e: Exception) {
    println("⚠️ anyValueList2 failed")
  }
}


@Serializable(with = AnyValue.Companion.Serializer::class)
sealed interface AnyValue {

  @JvmInline
  @Serializable
  value class Single(val value: String) : AnyValue

  @JvmInline
  @Serializable
  value class Multi(val values: List<String>) : AnyValue

  @JvmInline
  @Serializable
  value class Complex(val values: Map<String, String>) : AnyValue

  @JvmInline
  @Serializable
  value class Unknown(val value: JsonElement) : AnyValue

  companion object {
    object Serializer : JsonContentPolymorphicSerializer<AnyValue>(AnyValue::class) {

      override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out AnyValue> =
        when {
          element is JsonArray && element.all { it is JsonPrimitive && it.isString }         -> {
            println("selected Multi.serializer()")
            Multi.serializer()
          }

          element is JsonObject && element.values.all { it is JsonPrimitive && it.isString } -> {
            println("selected Complex.serializer()")
            Complex.serializer()
          }

          element is JsonPrimitive && element.isString                                       -> {
            println("selected Single.serializer()")
            Single.serializer()
          }

          else                                                                               -> {
            println("selected Unknown.serializer()")
            Unknown.serializer()
          }
        }
    }
  }
}
selected Single.serializer()
Single(value=I'm a string)
selected Complex.serializer()
Exception in thread "main" 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:322)
	at kotlinx.serialization.internal.TaggedDecoder.decodeInline(Tagged.kt:213)
	at AnyValue$Complex$$serializer.deserialize-0-2muUA(main.kt:30)
	at AnyValue$Complex$$serializer.deserialize(main.kt:30)
	at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61)
	at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52)
	at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:25)
	at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:115)
	at kotlinx.serialization.json.JsonContentPolymorphicSerializer.deserialize(JsonContentPolymorphicSerializer.kt:93)
	at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:70)
	at kotlinx.serialization.json.Json.decodeFromString(Json.kt:95)
	at MainKt.main(main.kt:86)
	at MainKt.main(main.kt)

Expected behavior

  • """ {"id": "1", "name": "object"} """ can be decoded to AnyValue.Complex
  • """["list", "of", "strings"]""" can be decoded to AnyValue.Multi

Environment

  • Kotlin version: 1.8.0
  • Library version: 1.4.1
  • Kotlin platforms: JVM
  • Gradle version: 7.8
@sandwwraith
Copy link
Member

Also #2049

sandwwraith added a commit that referenced this issue Mar 20, 2023
- Value class is located at top-level, but wraps non-primitive and thus does not fall in 'primitive on top-level' branch
- Value class is a subclass in a polymorphic hierarchy, but either is primitive or explicitly recorded without type info

Note that type info is omitted in the latter case and 'can't add type info to primitive' error is not thrown deliberately, as
there seems to be use-cases for that.

Fixes #1774
Fixes #2159
xBaank pushed a commit to xBaank/kotlinx.serialization that referenced this issue Apr 20, 2023
- Value class is located at top-level, but wraps non-primitive and thus does not fall in 'primitive on top-level' branch
- Value class is a subclass in a polymorphic hierarchy, but either is primitive or explicitly recorded without type info

Note that type info is omitted in the latter case and 'can't add type info to primitive' error is not thrown deliberately, as
there seems to be use-cases for that.

Fixes Kotlin#1774
Fixes Kotlin#2159
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants