Skip to content

Commit ad9ddd1

Browse files
authored
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
1 parent afebbcb commit ad9ddd1

File tree

9 files changed

+41
-8
lines changed

9 files changed

+41
-8
lines changed

docs/basic-serialization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ the `null` value to it.
534534

535535
```text
536536
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
537-
Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values.
537+
Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value.
538538
```
539539

540540
<!--- TEST LINES_START -->

formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
2929
val enum: SampleEnum?
3030
)
3131

32+
@Serializable
33+
class Uncoercable(
34+
val s: String
35+
)
36+
37+
@Serializable
38+
class UncoercableEnum(
39+
val e: SampleEnum
40+
)
41+
3242
val json = Json {
3343
coerceInputValues = true
3444
isLenient = true
@@ -112,4 +122,24 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
112122
decoded = decodeFromString<NullableEnumHolder>("""{"enum": OptionA}""")
113123
assertEquals(SampleEnum.OptionA, decoded.enum)
114124
}
125+
126+
@Test
127+
fun propertiesWithoutDefaultValuesDoNotChangeErrorMsg() {
128+
val json2 = Json(json) { coerceInputValues = false }
129+
parametrizedTest { mode ->
130+
val e1 = assertFailsWith<SerializationException>() { json.decodeFromString<Uncoercable>("""{"s":null}""", mode) }
131+
val e2 = assertFailsWith<SerializationException>() { json2.decodeFromString<Uncoercable>("""{"s":null}""", mode) }
132+
assertEquals(e2.message, e1.message)
133+
}
134+
}
135+
136+
@Test
137+
fun propertiesWithoutDefaultValuesDoNotChangeErrorMsgEnum() {
138+
val json2 = Json(json) { coerceInputValues = false }
139+
parametrizedTest { mode ->
140+
val e1 = assertFailsWith<SerializationException> { json.decodeFromString<UncoercableEnum>("""{"e":"UNEXPECTED"}""", mode) }
141+
val e2 = assertFailsWith<SerializationException> { json2.decodeFromString<UncoercableEnum>("""{"e":"UNEXPECTED"}""", mode) }
142+
assertEquals(e2.message, e1.message)
143+
}
144+
}
115145
}

formats/json/commonMain/src/kotlinx/serialization/json/Json.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public class JsonBuilder internal constructor(json: Json) {
287287
public var prettyPrintIndent: String = json.configuration.prettyPrintIndent
288288

289289
/**
290-
* Enables coercing incorrect JSON values to the default property value in the following cases:
290+
* Enables coercing incorrect JSON values to the default property value (if exists) in the following cases:
291291
* 1. JSON value is `null` but the property type is non-nullable.
292292
* 2. Property type is an enum type, but JSON value contains unknown enum member.
293293
*

formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,14 @@ internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String,
110110

111111
@OptIn(ExperimentalSerializationApi::class)
112112
internal inline fun Json.tryCoerceValue(
113-
elementDescriptor: SerialDescriptor,
113+
descriptor: SerialDescriptor,
114+
index: Int,
114115
peekNull: (consume: Boolean) -> Boolean,
115116
peekString: () -> String?,
116117
onEnumCoercing: () -> Unit = {}
117118
): Boolean {
119+
if (!descriptor.isElementOptional(index)) return false
120+
val elementDescriptor = descriptor.getElementDescriptor(index)
118121
if (!elementDescriptor.isNullable && peekNull(true)) return true
119122
if (elementDescriptor.kind == SerialKind.ENUM) {
120123
if (elementDescriptor.isNullable && peekNull(false)) {

formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ internal open class StreamingJsonDecoder(
213213
* Checks whether JSON has `null` value for non-null property or unknown enum value for enum property
214214
*/
215215
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int): Boolean = json.tryCoerceValue(
216-
descriptor.getElementDescriptor(index),
216+
descriptor, index,
217217
{ lexer.tryConsumeNull(it) },
218218
{ lexer.peekString(configuration.isLenient) },
219219
{ lexer.consumeString() /* skip unknown enum string*/ }

formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ private open class JsonTreeDecoder(
190190
*/
191191
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean =
192192
json.tryCoerceValue(
193-
descriptor.getElementDescriptor(index),
193+
descriptor, index,
194194
{ currentElement(tag) is JsonNull },
195195
{ (currentElement(tag) as? JsonPrimitive)?.contentOrNull }
196196
)

formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.jvm.*
1111
import kotlin.math.*
1212

1313
internal const val lenientHint = "Use 'isLenient = true' in 'Json {}' builder to accept non-compliant JSON."
14-
internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values."
14+
internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value."
1515
internal const val specialFlowingValuesHint =
1616
"It is possible to deserialize them using 'JsonBuilder.allowSpecialFloatingPointValues = true'"
1717
internal const val ignoreUnknownKeysHint = "Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys."

formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private open class DynamicInput(
7373

7474
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean =
7575
json.tryCoerceValue(
76-
descriptor.getElementDescriptor(index),
76+
descriptor, index,
7777
{ getByTag(tag) == null },
7878
{ getByTag(tag) as? String }
7979
)

guide/test/BasicSerializationTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class BasicSerializationTest {
110110
fun testExampleClasses12() {
111111
captureOutput("ExampleClasses12") { example.exampleClasses12.main() }.verifyOutputLinesStart(
112112
"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",
113-
"Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values."
113+
"Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value."
114114
)
115115
}
116116

0 commit comments

Comments
 (0)