diff options
author | Leonid Startsev <sandwwraith@users.noreply.github.com> | 2023-12-18 15:09:29 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-18 15:09:29 +0100 |
commit | ad9ddd10e9fea9660fe7b024cd6e6399d4922db5 (patch) | |
tree | 87758fbce998b7e63798a9b2b5fbfd6f4b5e7d8b | |
parent | afebbcbb6a66085915a62b6e61981db8f5922876 (diff) | |
download | kotlinx.serialization-ad9ddd10e9fea9660fe7b024cd6e6399d4922db5.tar.gz |
Do not try to coerce input values for properties (#2530)
Do not try to coerce input values for properties that do not have default values.
Trying so leads to confusing errors about missing values despite a json key actually
present in the input.
Fixes #2529
9 files changed, 41 insertions, 8 deletions
diff --git a/docs/basic-serialization.md b/docs/basic-serialization.md index 3853376e..96e70981 100644 --- a/docs/basic-serialization.md +++ b/docs/basic-serialization.md @@ -534,7 +534,7 @@ 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 at path: $.language -Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values. +Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value. ``` <!--- TEST LINES_START --> diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt index ecb946cb..3d7c3322 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt @@ -29,6 +29,16 @@ class JsonCoerceInputValuesTest : JsonTestBase() { val enum: SampleEnum? ) + @Serializable + class Uncoercable( + val s: String + ) + + @Serializable + class UncoercableEnum( + val e: SampleEnum + ) + val json = Json { coerceInputValues = true isLenient = true @@ -112,4 +122,24 @@ class JsonCoerceInputValuesTest : JsonTestBase() { decoded = decodeFromString<NullableEnumHolder>("""{"enum": OptionA}""") assertEquals(SampleEnum.OptionA, decoded.enum) } + + @Test + fun propertiesWithoutDefaultValuesDoNotChangeErrorMsg() { + val json2 = Json(json) { coerceInputValues = false } + parametrizedTest { mode -> + val e1 = assertFailsWith<SerializationException>() { json.decodeFromString<Uncoercable>("""{"s":null}""", mode) } + val e2 = assertFailsWith<SerializationException>() { json2.decodeFromString<Uncoercable>("""{"s":null}""", mode) } + assertEquals(e2.message, e1.message) + } + } + + @Test + fun propertiesWithoutDefaultValuesDoNotChangeErrorMsgEnum() { + val json2 = Json(json) { coerceInputValues = false } + parametrizedTest { mode -> + val e1 = assertFailsWith<SerializationException> { json.decodeFromString<UncoercableEnum>("""{"e":"UNEXPECTED"}""", mode) } + val e2 = assertFailsWith<SerializationException> { json2.decodeFromString<UncoercableEnum>("""{"e":"UNEXPECTED"}""", mode) } + assertEquals(e2.message, e1.message) + } + } } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt index 26c376ef..a510e8a3 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt @@ -287,7 +287,7 @@ public class JsonBuilder internal constructor(json: Json) { public var prettyPrintIndent: String = json.configuration.prettyPrintIndent /** - * Enables coercing incorrect JSON values to the default property value in the following cases: + * Enables coercing incorrect JSON values to the default property value (if exists) in the following cases: * 1. JSON value is `null` but the property type is non-nullable. * 2. Property type is an enum type, but JSON value contains unknown enum member. * 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 8acd8fc4..9128f3a2 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt @@ -110,11 +110,14 @@ internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String, @OptIn(ExperimentalSerializationApi::class) internal inline fun Json.tryCoerceValue( - elementDescriptor: SerialDescriptor, + descriptor: SerialDescriptor, + index: Int, peekNull: (consume: Boolean) -> Boolean, peekString: () -> String?, onEnumCoercing: () -> Unit = {} ): Boolean { + if (!descriptor.isElementOptional(index)) return false + val elementDescriptor = descriptor.getElementDescriptor(index) if (!elementDescriptor.isNullable && peekNull(true)) return true if (elementDescriptor.kind == SerialKind.ENUM) { if (elementDescriptor.isNullable && peekNull(false)) { 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 0018fce1..caa1f4a5 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt @@ -213,7 +213,7 @@ internal open class StreamingJsonDecoder( * Checks whether JSON has `null` value for non-null property or unknown enum value for enum property */ private fun coerceInputValue(descriptor: SerialDescriptor, index: Int): Boolean = json.tryCoerceValue( - descriptor.getElementDescriptor(index), + descriptor, index, { lexer.tryConsumeNull(it) }, { lexer.peekString(configuration.isLenient) }, { lexer.consumeString() /* skip unknown enum string*/ } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt index aedfb95f..690b35e1 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt @@ -190,7 +190,7 @@ private open class JsonTreeDecoder( */ private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean = json.tryCoerceValue( - descriptor.getElementDescriptor(index), + descriptor, index, { currentElement(tag) is JsonNull }, { (currentElement(tag) as? JsonPrimitive)?.contentOrNull } ) 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 c83bdef9..f90ee1a0 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 @@ -11,7 +11,7 @@ import kotlin.jvm.* import kotlin.math.* internal const val lenientHint = "Use 'isLenient = true' in 'Json {}' builder to accept non-compliant JSON." -internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values." +internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value." internal const val specialFlowingValuesHint = "It is possible to deserialize them using 'JsonBuilder.allowSpecialFloatingPointValues = true'" internal const val ignoreUnknownKeysHint = "Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys." diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt index 86c7a85c..1ff1e40d 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt @@ -73,7 +73,7 @@ private open class DynamicInput( private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean = json.tryCoerceValue( - descriptor.getElementDescriptor(index), + descriptor, index, { getByTag(tag) == null }, { getByTag(tag) as? String } ) diff --git a/guide/test/BasicSerializationTest.kt b/guide/test/BasicSerializationTest.kt index dc89feb6..11f9e9f2 100644 --- a/guide/test/BasicSerializationTest.kt +++ b/guide/test/BasicSerializationTest.kt @@ -110,7 +110,7 @@ class BasicSerializationTest { fun testExampleClasses12() { captureOutput("ExampleClasses12") { example.exampleClasses12.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 at path: $.language", - "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values." + "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value." ) } |