diff --git a/docs/basic-serialization.md b/docs/basic-serialization.md index 3853376ee..96e709817 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. ``` 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 ecb946cb7..3d7c33227 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("""{"enum": OptionA}""") assertEquals(SampleEnum.OptionA, decoded.enum) } + + @Test + fun propertiesWithoutDefaultValuesDoNotChangeErrorMsg() { + val json2 = Json(json) { coerceInputValues = false } + parametrizedTest { mode -> + val e1 = assertFailsWith() { json.decodeFromString("""{"s":null}""", mode) } + val e2 = assertFailsWith() { json2.decodeFromString("""{"s":null}""", mode) } + assertEquals(e2.message, e1.message) + } + } + + @Test + fun propertiesWithoutDefaultValuesDoNotChangeErrorMsgEnum() { + val json2 = Json(json) { coerceInputValues = false } + parametrizedTest { mode -> + val e1 = assertFailsWith { json.decodeFromString("""{"e":"UNEXPECTED"}""", mode) } + val e2 = assertFailsWith { json2.decodeFromString("""{"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 26c376ef6..a510e8a30 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 8acd8fc47..9128f3a27 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 0018fce10..caa1f4a52 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 aedfb95f3..690b35e1f 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 c83bdef97..f90ee1a0e 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 86c7a85cb..1ff1e40da 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 dc89feb69..11f9e9f28 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." ) }