summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVsevolod Tolstopyatov <qwwdfsad@gmail.com>2022-05-11 17:13:57 +0600
committerGitHub <noreply@github.com>2022-05-11 14:13:57 +0300
commitfb02e66ce516673d2e2f8f6216f4dd10d4bbef3c (patch)
tree11231e935f05a85a6c5ca2420e438efc90e0d9f6
parenta46299e38f5136075946b9d7accb701c31246f9b (diff)
downloadkotlinx.serialization-fb02e66ce516673d2e2f8f6216f4dd10d4bbef3c.tar.gz
Incorporate JsonPath into exception messages (#1841)
Fixes #1817 Fixes #1137
-rw-r--r--core/api/kotlinx-serialization-core.api4
-rw-r--r--core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt4
-rw-r--r--docs/basic-serialization.md8
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt32
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt4
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonPath.kt141
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt42
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt16
-rw-r--r--formats/json/commonTest/src/kotlinx/serialization/JsonPathTest.kt164
-rw-r--r--formats/json/jvmTest/src/kotlinx/serialization/features/JsonLazySequenceTest.kt (renamed from formats/json/jvmTest/src/kotlinx/serialization/features/JsonStreamFlowTest.kt)2
-rw-r--r--formats/json/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt41
-rw-r--r--guide/test/BasicSerializationTest.kt8
12 files changed, 427 insertions, 39 deletions
diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api
index 4aab661b..ede49f69 100644
--- a/core/api/kotlinx-serialization-core.api
+++ b/core/api/kotlinx-serialization-core.api
@@ -339,7 +339,7 @@ public abstract class kotlinx/serialization/encoding/AbstractDecoder : kotlinx/s
public fun decodeFloat ()F
public final fun decodeFloatElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)F
public fun decodeInline (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/Decoder;
- public final fun decodeInlineElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/Decoder;
+ public fun decodeInlineElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/Decoder;
public fun decodeInt ()I
public final fun decodeIntElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)I
public fun decodeLong ()J
@@ -349,7 +349,7 @@ public abstract class kotlinx/serialization/encoding/AbstractDecoder : kotlinx/s
public final fun decodeNullableSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;)Ljava/lang/Object;
public fun decodeNullableSerializableValue (Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object;
public fun decodeSequentially ()Z
- public final fun decodeSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;)Ljava/lang/Object;
+ public fun decodeSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;)Ljava/lang/Object;
public fun decodeSerializableValue (Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object;
public fun decodeSerializableValue (Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;)Ljava/lang/Object;
public static synthetic fun decodeSerializableValue$default (Lkotlinx/serialization/encoding/AbstractDecoder;Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object;
diff --git a/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt b/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt
index e231d558..8e2799c9 100644
--- a/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt
+++ b/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt
@@ -57,12 +57,12 @@ public abstract class AbstractDecoder : Decoder, CompositeDecoder {
final override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char = decodeChar()
final override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String = decodeString()
- final override fun decodeInlineElement(
+ override fun decodeInlineElement(
descriptor: SerialDescriptor,
index: Int
): Decoder = decodeInline(descriptor.getElementDescriptor(index))
- final override fun <T> decodeSerializableElement(
+ override fun <T> decodeSerializableElement(
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T>,
diff --git a/docs/basic-serialization.md b/docs/basic-serialization.md
index 14a3e82d..7425be7a 100644
--- a/docs/basic-serialization.md
+++ b/docs/basic-serialization.md
@@ -297,7 +297,7 @@ fun main() {
It produces the exception:
```text
-Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing
+Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing at path: $
```
<!--- TEST LINES_START -->
@@ -383,7 +383,7 @@ fun main() {
We get the following exception.
```text
-Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing
+Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing at path: $
```
<!--- TEST LINES_START -->
@@ -411,7 +411,7 @@ Attempts to explicitly specify its value in the serial format, even if the speci
value is equal to the default one, produces the following exception.
```text
-Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language'.
+Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language' at path: $.name
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
```
@@ -493,7 +493,7 @@ Even though the `language` property has a default value, it is still an error to
the `null` value to it.
```text
-Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found.
+Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language
Use 'coerceInputValues = true' in 'Json {}` builder to coerce nulls to default values.
```
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt
index 1f57de47..d1698db2 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonExceptions.kt
@@ -38,20 +38,29 @@ internal fun InvalidFloatingPointEncoded(value: Number, output: String) = JsonEn
"Current output: ${output.minify()}"
)
-internal fun InvalidFloatingPointEncoded(value: Number, key: String, output: String) =
- JsonEncodingException(unexpectedFpErrorMessage(value, key, output))
-
-internal fun InvalidFloatingPointDecoded(value: Number, key: String, output: String) =
- JsonDecodingException(-1, unexpectedFpErrorMessage(value, key, output))
// Extension on JSON reader and fail immediately
internal fun AbstractJsonLexer.throwInvalidFloatingPointDecoded(result: Number): Nothing {
fail("Unexpected special floating-point value $result. By default, " +
- "non-finite floating point values are prohibited because they do not conform JSON specification. " +
- specialFlowingValuesHint
- )
+ "non-finite floating point values are prohibited because they do not conform JSON specification",
+ hint = specialFlowingValuesHint)
}
+@OptIn(ExperimentalSerializationApi::class)
+internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEncodingException(
+ "Value of type '${keyDescriptor.serialName}' can't be used in JSON as a key in the map. " +
+ "It should have either primitive or enum kind, but its kind is '${keyDescriptor.kind}'.\n" +
+ allowStructuredMapKeysHint
+)
+
+// Exceptions for tree-based decoder
+
+internal fun InvalidFloatingPointEncoded(value: Number, key: String, output: String) =
+ JsonEncodingException(unexpectedFpErrorMessage(value, key, output))
+
+internal fun InvalidFloatingPointDecoded(value: Number, key: String, output: String) =
+ JsonDecodingException(-1, unexpectedFpErrorMessage(value, key, output))
+
private fun unexpectedFpErrorMessage(value: Number, key: String, output: String): String {
return "Unexpected special floating-point value $value with key $key. By default, " +
"non-finite floating point values are prohibited because they do not conform JSON specification. " +
@@ -66,13 +75,6 @@ internal fun UnknownKeyException(key: String, input: String) = JsonDecodingExcep
"Current input: ${input.minify()}"
)
-@OptIn(ExperimentalSerializationApi::class)
-internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEncodingException(
- "Value of type '${keyDescriptor.serialName}' can't be used in JSON as a key in the map. " +
- "It should have either primitive or enum kind, but its kind is '${keyDescriptor.kind}'.\n" +
- allowStructuredMapKeysHint
-)
-
private fun CharSequence.minify(offset: Int = -1): CharSequence {
if (length < 200) return this
if (offset == -1) {
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
index a8d28997..93e604a7 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
@@ -57,10 +57,10 @@ internal fun SerialDescriptor.getJsonNameIndex(json: Json, name: String): Int {
* Throws on [CompositeDecoder.UNKNOWN_NAME]
*/
@OptIn(ExperimentalSerializationApi::class)
-internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String): Int {
+internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String, suffix: String = ""): Int {
val index = getJsonNameIndex(json, name)
if (index == CompositeDecoder.UNKNOWN_NAME)
- throw SerializationException("$serialName does not contain element with name '$name'")
+ throw SerializationException("$serialName does not contain element with name '$name'$suffix")
return index
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonPath.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonPath.kt
new file mode 100644
index 00000000..4e055b23
--- /dev/null
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonPath.kt
@@ -0,0 +1,141 @@
+package kotlinx.serialization.json.internal
+
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.internal.*
+
+/**
+ * Internal representation of the current JSON path.
+ * It is stored as the array of serial descriptors (for regular classes)
+ * and `Any?` in case of Map keys.
+ *
+ * Example of the state when decoding the list
+ * ```
+ * class Foo(val a: Int, val l: List<String>)
+ *
+ * // {"l": ["a", "b", "c"] }
+ *
+ * Current path when decoding array elements:
+ * Foo.descriptor, List(String).descriptor
+ * 1 (index of the 'l'), 2 (index of currently being decoded "c")
+ * ```
+ */
+internal class JsonPath {
+
+ // Tombstone indicates that we are within a map, but the map key is currently being decoded.
+ // It is also used to overwrite a previous map key to avoid memory leaks and misattribution.
+ object Tombstone
+
+ /*
+ * Serial descriptor, map key or the tombstone for map key
+ */
+ private var currentObjectPath = arrayOfNulls<Any?>(8)
+ /*
+ * Index is a small state-machine used to determine the state of the path:
+ * >=0 -> index of the element being decoded with the outer class currentObjectPath[currentDepth]
+ * -1 -> nested elements are not yet decoded
+ * -2 -> the map is being decoded and both its descriptor AND the last key were added to the path.
+ *
+ * -2 is effectively required to specify that two slots has been claimed and both should be
+ * cleaned up when the decoding is done.
+ * The cleanup is essential in order to avoid memory leaks for huge strings and structured keys.
+ */
+ private var indicies = IntArray(8) { -1 }
+ private var currentDepth = -1
+
+ // Invoked when class is started being decoded
+ fun pushDescriptor(sd: SerialDescriptor) {
+ val depth = ++currentDepth
+ if (depth == currentObjectPath.size) {
+ resize()
+ }
+ currentObjectPath[depth] = sd
+ }
+
+ // Invoked when index-th element of the current descriptor is being decoded
+ fun updateDescriptorIndex(index: Int) {
+ indicies[currentDepth] = index
+ }
+
+ /*
+ * For maps we cannot use indicies and should use the key as an element of the path instead.
+ * The key can be even an object (e.g. in a case of 'allowStructuredMapKeys') where
+ * 'toString' is way too heavy or have side-effects.
+ * For that we are storing the key instead.
+ */
+ fun updateCurrentMapKey(key: Any?) {
+ // idx != -2 -> this is the very first key being added
+ if (indicies[currentDepth] != -2 && ++currentDepth == currentObjectPath.size) {
+ resize()
+ }
+ currentObjectPath[currentDepth] = key
+ indicies[currentDepth] = -2
+ }
+
+ /** Used to indicate that we are in the process of decoding the key itself and can't specify it in path */
+ fun resetCurrentMapKey() {
+ if (indicies[currentDepth] == -2) {
+ currentObjectPath[currentDepth] = Tombstone
+ }
+ }
+
+ fun popDescriptor() {
+ // When we are ending map, we pop the last key and the outer field as well
+ val depth = currentDepth
+ if (indicies[depth] == -2) {
+ indicies[depth] = -1
+ currentDepth--
+ }
+ // Guard against top-level maps
+ if (currentDepth != -1) {
+ // No need to clean idx up as it was already cleaned by updateDescriptorIndex(DECODE_DONE)
+ currentDepth--
+ }
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ fun getPath(): String {
+ return buildString {
+ append("$")
+ repeat(currentDepth + 1) {
+ val element = currentObjectPath[it]
+ if (element is SerialDescriptor) {
+ if (element.kind == StructureKind.LIST) {
+ if (indicies[it] != -1) {
+ append("[")
+ append(indicies[it])
+ append("]")
+ }
+ } else {
+ val idx = indicies[it]
+ // If an actual element is being decoded
+ if (idx >= 0) {
+ append(".")
+ append(element.getElementName(idx))
+ }
+ }
+ } else if (element !== Tombstone) {
+ append("[")
+ // All non-indicies should be properly quoted by JsonPath convention
+ append("'")
+ // Else -- map key
+ append(element)
+ append("'")
+ append("]")
+ }
+ }
+ }
+ }
+
+
+ @OptIn(ExperimentalSerializationApi::class)
+ private fun prettyString(it: Any?) = (it as? SerialDescriptor)?.serialName ?: it.toString()
+
+ private fun resize() {
+ val newSize = currentDepth * 2
+ currentObjectPath = currentObjectPath.copyOf(newSize)
+ indicies = indicies.copyOf(newSize)
+ }
+
+ override fun toString(): String = getPath()
+}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
index 5d6b5bf6..bf229044 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
@@ -32,12 +32,18 @@ internal open class StreamingJsonDecoder(
override fun decodeJsonElement(): JsonElement = JsonTreeReader(json.configuration, lexer).read()
+ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
- return decodeSerializableValuePolymorphic(deserializer)
+ try {
+ return decodeSerializableValuePolymorphic(deserializer)
+ } catch (e: MissingFieldException) {
+ throw MissingFieldException(e.message + " at path: " + lexer.path.getPath(), e)
+ }
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
val newMode = json.switchMode(descriptor)
+ lexer.path.pushDescriptor(descriptor)
lexer.consumeNextToken(newMode.begin)
checkLeadingComma()
return when (newMode) {
@@ -63,7 +69,10 @@ internal open class StreamingJsonDecoder(
if (json.configuration.ignoreUnknownKeys && descriptor.elementsCount == 0) {
skipLeftoverElements(descriptor)
}
+ // First consume the object so we know it's correct
lexer.consumeNextToken(mode.end)
+ // Then cleanup the path
+ lexer.path.popDescriptor()
}
private fun skipLeftoverElements(descriptor: SerialDescriptor) {
@@ -87,12 +96,37 @@ internal open class StreamingJsonDecoder(
}
}
+ override fun <T> decodeSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ deserializer: DeserializationStrategy<T>,
+ previousValue: T?
+ ): T {
+ val isMapKey = mode == WriteMode.MAP && index and 1 == 0
+ // Reset previous key
+ if (isMapKey) {
+ lexer.path.resetCurrentMapKey()
+ }
+ // Deserialize the key
+ val value = super.decodeSerializableElement(descriptor, index, deserializer, previousValue)
+ // Put the key to the path
+ if (isMapKey) {
+ lexer.path.updateCurrentMapKey(value)
+ }
+ return value
+ }
+
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
- return when (mode) {
+ val index = when (mode) {
WriteMode.OBJ -> decodeObjectIndex(descriptor)
WriteMode.MAP -> decodeMapIndex()
else -> decodeListIndex() // Both for LIST and default polymorphic
}
+ // The element of the next index that will be decoded
+ if (mode != WriteMode.MAP) {
+ lexer.path.updateDescriptorIndex(index)
+ }
+ return index
}
private fun decodeMapIndex(): Int {
@@ -162,6 +196,8 @@ internal open class StreamingJsonDecoder(
if (configuration.ignoreUnknownKeys) {
lexer.skipElement(configuration.isLenient)
} else {
+ // Here we cannot properly update json path indicies
+ // as we do not have a proper SerialDecriptor in our hands
lexer.failOnUnknownKey(key)
}
return lexer.tryConsumeComma()
@@ -262,7 +298,7 @@ internal open class StreamingJsonDecoder(
else super.decodeInline(inlineDescriptor)
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
- return enumDescriptor.getJsonNameIndexOrThrow(json, decodeString())
+ return enumDescriptor.getJsonNameIndexOrThrow(json, decodeString(), " at path " + lexer.path.getPath())
}
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
index 82881ef7..5e1eee7c 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
@@ -137,6 +137,9 @@ internal abstract class AbstractJsonLexer {
@JvmField
protected var currentPosition: Int = 0 // position in source
+ @JvmField
+ val path = JsonPath()
+
open fun ensureHaveChars() {}
fun isNotEof(): Boolean = peekNextToken() != TC_EOF
@@ -199,7 +202,7 @@ internal abstract class AbstractJsonLexer {
protected fun unexpectedToken(expected: Char) {
--currentPosition // To properly handle null
if (currentPosition >= 0 && expected == STRING && consumeStringLenient() == NULL) {
- fail("Expected string literal but 'null' literal was found.\n$coerceInputValuesHint", currentPosition - 4)
+ fail("Expected string literal but 'null' literal was found", currentPosition - 4, coerceInputValuesHint)
}
fail(charToTokenClass(expected))
}
@@ -488,7 +491,7 @@ internal abstract class AbstractJsonLexer {
TC_END_LIST -> {
if (tokenStack.last() != TC_BEGIN_LIST) throw JsonDecodingException(
currentPosition,
- "found ] instead of }",
+ "found ] instead of } at path: $path",
source
)
tokenStack.removeLast()
@@ -496,7 +499,7 @@ internal abstract class AbstractJsonLexer {
TC_END_OBJ -> {
if (tokenStack.last() != TC_BEGIN_OBJ) throw JsonDecodingException(
currentPosition,
- "found } instead of ]",
+ "found } instead of ] at path: $path",
source
)
tokenStack.removeLast()
@@ -517,11 +520,12 @@ internal abstract class AbstractJsonLexer {
// but still would like an error to point to the beginning of the key, so we are backtracking it
val processed = substring(0, currentPosition)
val lastIndexOf = processed.lastIndexOf(key)
- fail("Encountered an unknown key '$key'.\n$ignoreUnknownKeysHint", lastIndexOf)
+ fail("Encountered an unknown key '$key'", lastIndexOf, ignoreUnknownKeysHint)
}
- fun fail(message: String, position: Int = currentPosition): Nothing {
- throw JsonDecodingException(position, message, source)
+ fun fail(message: String, position: Int = currentPosition, hint: String = ""): Nothing {
+ val hintMessage = if (hint.isEmpty()) "" else "\n$hint"
+ throw JsonDecodingException(position, message + " at path: " + path.getPath() + hintMessage, source)
}
fun consumeNumericLiteral(): Long {
diff --git a/formats/json/commonTest/src/kotlinx/serialization/JsonPathTest.kt b/formats/json/commonTest/src/kotlinx/serialization/JsonPathTest.kt
new file mode 100644
index 00000000..8d31ba22
--- /dev/null
+++ b/formats/json/commonTest/src/kotlinx/serialization/JsonPathTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization
+
+import kotlinx.serialization.json.*
+import kotlinx.serialization.test.*
+import kotlin.test.*
+
+class JsonPathTest : JsonTestBase() {
+
+ @Serializable
+ class Outer(val a: Int, val i: Inner)
+
+ @Serializable
+ class Inner(val a: Int, val b: String, val c: List<String>, val d: Map<Int, Box>)
+
+ @Serializable
+ class Box(val s: String)
+
+ @Test
+ fun testBasicError() {
+ expectPath("$.a") { Json.decodeFromString<Outer>("""{"a":foo}""") }
+ expectPath("$.i") { Json.decodeFromString<Outer>("""{"a":42, "i":[]}""") }
+ expectPath("$.i.b") { Json.decodeFromString<Outer>("""{"a":42, "i":{"a":43, "b":42}""") }
+ expectPath("$.i.b") { Json.decodeFromString<Outer>("""{"a":42, "i":{"b":42}""") }
+ }
+
+ @Test
+ fun testMissingKey() {
+ expectPath("$.i.d['1']") { Json.decodeFromString<Outer>("""{"a":42, "i":{"d":{1:{}}""") }
+ }
+
+ @Test
+ fun testUnknownKeyIsProperlyReported() {
+ expectPath("$.i") { Json.decodeFromString<Outer>("""{"a":42, "i":{"foo":42}""") }
+ expectPath("$") { Json.decodeFromString<Outer>("""{"x":{}, "a": 42}""") }
+ // The only place we have misattribution in
+ // Json.decodeFromString<Outer>("""{"a":42, "x":{}}""")
+ }
+
+ @Test
+ fun testMalformedRootObject() {
+ expectPath("$") { Json.decodeFromString<Outer>("""{{""") }
+ }
+
+ @Test
+ fun testArrayIndex() {
+ expectPath("$.i.c[1]") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "c": ["a", 2]}""") }
+ expectPath("$[2]") { Json.decodeFromString<List<String>>("""["a", "2", 3]""") }
+ }
+
+ @Test
+ fun testArrayIndexMalformedArray() {
+ // Also zeroes as we cannot distinguish what exactly wen wrong is such cases
+ expectPath("$.i.c[0]") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "c": [[""") }
+ expectPath("$[0]") { Json.decodeFromString<List<String>>("""[[""") }
+ // But we can here
+ expectPath("$.i.c\n") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "c": {}}}""") }
+ expectPath("$\n") { Json.decodeFromString<List<String>>("""{""") }
+ }
+
+ @Test
+ fun testMapKey() {
+ expectPath("$.i.d\n") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "d": {"foo": {}}""") }
+ expectPath("$.i.d\n") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "d": {42: {"s":"s"}, 42.0:{}}""") }
+ expectPath("$\n") { Json.decodeFromString<Map<Int, String>>("""{"foo":"bar"}""") }
+ expectPath("$\n") { Json.decodeFromString<Map<Int, String>>("""{42:"bar", "foo":"bar"}""") }
+ expectPath("$['42']['foo']") { Json.decodeFromString<Map<Int, Map<String, Int>>>("""{42: {"foo":"bar"}""") }
+ }
+
+ @Test
+ fun testMalformedMap() {
+ expectPath("$.i.d\n") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "d": []""") }
+ expectPath("$\n") { Json.decodeFromString<Map<Int, String>>("""[]""") }
+ }
+
+ @Test
+ fun testMapValue() {
+ expectPath("$.i.d['42']\n") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "d": {42: {"xx":"bar"}}""") }
+ expectPath("$.i.d['43']\n") { Json.decodeFromString<Outer>("""{"a":42, "i":{ "d": {42: {"s":"s"}, 43: {"xx":"bar"}}}""") }
+ expectPath("$['239']") { Json.decodeFromString<Map<Int, String>>("""{239:bar}""") }
+ }
+
+ @Serializable
+ class Fp(val d: Double)
+
+ @Test
+ fun testInvalidFp() {
+ expectPath("$.d") { Json.decodeFromString<Fp>("""{"d": NaN}""") }
+ }
+
+ @Serializable
+ class EH(val e: E)
+ enum class E
+
+ @Test
+ fun testUnknownEnum() {
+ expectPath("$.e") { Json.decodeFromString<EH>("""{"e": "foo"}""") }
+ }
+
+ @Serializable
+ @SerialName("f")
+ sealed class Sealed {
+
+ @Serializable
+ @SerialName("n")
+ class Nesting(val f: Sealed) : Sealed()
+
+ @Serializable
+ @SerialName("b")
+ class Box(val s: String) : Sealed()
+
+ @Serializable
+ @SerialName("d")
+ class DoubleNesting(val f: Sealed, val f2: Sealed) : Sealed()
+ }
+
+ // TODO use non-array polymorphism when https://github.com/Kotlin/kotlinx.serialization/issues/1839 is fixed
+ @Test
+ fun testHugeNestingToCheckResize() = jvmOnly {
+ val json = Json { useArrayPolymorphism = true }
+ var outer = Sealed.Nesting(Sealed.Box("value"))
+ repeat(100) {
+ outer = Sealed.Nesting(outer)
+ }
+ val str = json.encodeToString(Sealed.serializer(), outer)
+ // throw-away data
+ json.decodeFromString(Sealed.serializer(), str)
+
+ val malformed = str.replace("\"value\"", "42")
+ val expectedPath = "$" + ".value.f".repeat(101) + ".value.s"
+ expectPath(expectedPath) { json.decodeFromString(Sealed.serializer(), malformed) }
+ }
+
+ @Test
+ fun testDoubleNesting() = jvmOnly {
+ val json = Json { useArrayPolymorphism = true }
+ var outer1 = Sealed.Nesting(Sealed.Box("correct"))
+ repeat(64) {
+ outer1 = Sealed.Nesting(outer1)
+ }
+
+ var outer2 = Sealed.Nesting(Sealed.Box("incorrect"))
+ repeat(33) {
+ outer2 = Sealed.Nesting(outer2)
+ }
+
+ val str = json.encodeToString(Sealed.serializer(), Sealed.DoubleNesting(outer1, outer2))
+ // throw-away data
+ json.decodeFromString(Sealed.serializer(), str)
+
+ val malformed = str.replace("\"incorrect\"", "42")
+ val expectedPath = "$.value.f2" + ".value.f".repeat(34) + ".value.s"
+ expectPath(expectedPath) { json.decodeFromString(Sealed.serializer(), malformed) }
+ }
+
+ private inline fun expectPath(path: String, block: () -> Unit) {
+ val message = runCatching { block() }
+ .exceptionOrNull()!!.message!!
+ assertContains(message, path)
+ }
+}
diff --git a/formats/json/jvmTest/src/kotlinx/serialization/features/JsonStreamFlowTest.kt b/formats/json/jvmTest/src/kotlinx/serialization/features/JsonLazySequenceTest.kt
index 3de5a615..aad9a0f5 100644
--- a/formats/json/jvmTest/src/kotlinx/serialization/features/JsonStreamFlowTest.kt
+++ b/formats/json/jvmTest/src/kotlinx/serialization/features/JsonLazySequenceTest.kt
@@ -18,7 +18,7 @@ import org.junit.Test
import java.io.*
import kotlin.test.*
-class JsonStreamFlowTest {
+class JsonLazySequenceTest {
val json = Json
private suspend inline fun <reified T> Flow<T>.writeToStream(os: OutputStream) {
diff --git a/formats/json/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt b/formats/json/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt
new file mode 100644
index 00000000..287e4438
--- /dev/null
+++ b/formats/json/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.features
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.*
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.features.sealed.SealedChild
+import kotlinx.serialization.features.sealed.SealedParent
+import kotlinx.serialization.json.*
+import kotlinx.serialization.json.internal.JsonDecodingException
+import kotlinx.serialization.test.assertFailsWithMessage
+import org.junit.Test
+import java.io.*
+import kotlin.test.*
+
+class JsonSequencePathTest {
+
+ @Serializable
+ class NestedData(val s: String)
+
+ @Serializable
+ class Data(val data: NestedData)
+
+ @Test
+ fun testFailure() {
+ val source = """{"data":{"s":"value"}}{"data":{"s":42}}{notevenreached}""".toStream()
+ val iterator = Json.decodeToSequence<Data>(source).iterator()
+ iterator.next() // Ignore
+ assertFailsWithMessage<SerializationException>(
+ "Expected quotation mark '\"', but had '2' instead at path: \$.data.s"
+ ) { iterator.next() }
+ }
+
+ private fun String.toStream() = ByteArrayInputStream(encodeToByteArray())
+}
diff --git a/guide/test/BasicSerializationTest.kt b/guide/test/BasicSerializationTest.kt
index 88c01857..0024159d 100644
--- a/guide/test/BasicSerializationTest.kt
+++ b/guide/test/BasicSerializationTest.kt
@@ -51,7 +51,7 @@ class BasicSerializationTest {
@Test
fun testExampleClasses04() {
captureOutput("ExampleClasses04") { example.exampleClasses04.main() }.verifyOutputLinesStart(
- "Exception in thread \"main\" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing"
+ "Exception in thread \"main\" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing at path: $"
)
}
@@ -72,14 +72,14 @@ class BasicSerializationTest {
@Test
fun testExampleClasses07() {
captureOutput("ExampleClasses07") { example.exampleClasses07.main() }.verifyOutputLinesStart(
- "Exception in thread \"main\" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing"
+ "Exception in thread \"main\" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing at path: $"
)
}
@Test
fun testExampleClasses08() {
captureOutput("ExampleClasses08") { example.exampleClasses08.main() }.verifyOutputLinesStart(
- "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language'.",
+ "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language' at path: $.name",
"Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys."
)
}
@@ -101,7 +101,7 @@ class BasicSerializationTest {
@Test
fun testExampleClasses11() {
captureOutput("ExampleClasses11") { example.exampleClasses11.main() }.verifyOutputLinesStart(
- "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found.",
+ "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language",
"Use 'coerceInputValues = true' in 'Json {}` builder to coerce nulls to default values."
)
}