Skip to content

Commit 9adedb4

Browse files
authored
Fix incorrect enum coercion during deserialization from JsonElement (#2962)
in cases when a property is nullable and not optional, and explicitNulls is set to false. Fixes #2909
1 parent 27e352d commit 9adedb4

File tree

4 files changed

+59
-24
lines changed

4 files changed

+59
-24
lines changed

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,27 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
155155
fun testNullableEnumWithoutDefault() {
156156
val j = Json(json) { explicitNulls = false }
157157
parametrizedTest { mode ->
158-
assertEquals(NullableEnumHolder(null), j.decodeFromString("{}"))
159-
assertEquals(NullableEnumHolder(null), j.decodeFromString("""{"enum":"incorrect"}"""))
158+
assertEquals(NullableEnumHolder(null), j.decodeFromString("{}", mode))
159+
assertEquals(NullableEnumHolder(null), j.decodeFromString("""{"enum":"incorrect"}""", mode))
160160
}
161161
}
162162

163163
@Test
164164
fun testNullableEnumWithoutDefaultDoesNotCoerceExplicitly() {
165165
val j = Json(json) { explicitNulls = true }
166166
parametrizedTest { mode ->
167-
assertFailsWith<SerializationException> { j.decodeFromString<NullableEnumHolder>("{}") }
168-
assertFailsWith<SerializationException> { j.decodeFromString<NullableEnumHolder>("""{"enum":"incorrect"}""") }
167+
assertFailsWith<SerializationException> { j.decodeFromString<NullableEnumHolder>("{}", mode) }
168+
assertFailsWith<SerializationException> { j.decodeFromString<NullableEnumHolder>("""{"enum":"incorrect"}""", mode) }
169169
}
170170
}
171171

172172
@Test
173173
fun testNullableEnumWithDefault() {
174174
val j = Json(json) { explicitNulls = false }
175175
parametrizedTest { mode ->
176-
assertEquals(NullableEnumWithDefault(), j.decodeFromString("{}"))
177-
assertEquals(NullableEnumWithDefault(), j.decodeFromString("""{"e":"incorrect"}"""))
178-
assertEquals(NullableEnumWithDefault(null), j.decodeFromString("""{"e":null}"""))
176+
assertEquals(NullableEnumWithDefault(), j.decodeFromString("{}", mode))
177+
assertEquals(NullableEnumWithDefault(), j.decodeFromString("""{"e":"incorrect"}""", mode))
178+
assertEquals(NullableEnumWithDefault(null), j.decodeFromString("""{"e":null}""", mode))
179179
}
180180
}
181181
}

formats/json-tests/jsTest/src/kotlinx/serialization/json/JsonCoerceInputValuesDynamicTest.kt

+16-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ class JsonCoerceInputValuesDynamicTest {
1313
isLenient = true
1414
}
1515

16-
private fun <T> doTest(inputs: List<dynamic>, expected: T, serializer: KSerializer<T>) {
16+
private fun <T> doTest(inputs: List<dynamic>, expected: T, serializer: KSerializer<T>, jsonImpl: Json = json) {
1717
for (input in inputs) {
18-
assertEquals(expected, json.decodeFromDynamic(serializer, input), "Failed on input: $input")
18+
assertEquals(expected, jsonImpl.decodeFromDynamic(serializer, input), "Failed on input: $input")
1919
}
2020
}
2121

@@ -49,6 +49,20 @@ class JsonCoerceInputValuesDynamicTest {
4949
}
5050
}
5151

52+
@Test
53+
fun testUseNullWithImplicitNulls() {
54+
val withImplicitNulls = Json(json) { explicitNulls = false }
55+
doTest(
56+
listOf(
57+
js("""{}"""),
58+
js("""{"enum":"incorrect"}"""),
59+
),
60+
JsonCoerceInputValuesTest.NullableEnumHolder(null),
61+
JsonCoerceInputValuesTest.NullableEnumHolder.serializer(),
62+
withImplicitNulls
63+
)
64+
}
65+
5266
@Test
5367
fun testUseDefaultInMultipleCases() {
5468
val testData = mapOf<dynamic, JsonCoerceInputValuesTest.MultipleValues>(

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

+19-13
Original file line numberDiff line numberDiff line change
@@ -197,31 +197,35 @@ private open class JsonTreeDecoder(
197197
) : AbstractJsonTreeDecoder(json, value, polymorphicDiscriminator) {
198198
private var position = 0
199199
private var forceNull: Boolean = false
200-
/*
201-
* Checks whether JSON has `null` value for non-null property or unknown enum value for enum property
202-
*/
203-
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean =
204-
json.tryCoerceValue(
205-
descriptor, index,
206-
{ currentElement(tag) is JsonNull },
207-
{ (currentElement(tag) as? JsonPrimitive)?.contentOrNull }
208-
)
209200

210201
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
211202
while (position < descriptor.elementsCount) {
212203
val name = descriptor.getTag(position++)
213204
val index = position - 1
214205
forceNull = false
215-
if ((name in value || absenceIsNull(descriptor, index))
216-
&& (!configuration.coerceInputValues || !coerceInputValue(descriptor, index, name))
217-
) {
206+
207+
if (name in value || setForceNull(descriptor, index)) {
208+
// if forceNull is true, then decodeNotNullMark returns false and `null` is automatically inserted
209+
// by Decoder.decodeIfNullable
210+
if (!configuration.coerceInputValues) return index
211+
212+
if (json.tryCoerceValue(
213+
descriptor, index,
214+
{ currentElementOrNull(name) is JsonNull },
215+
{ (currentElementOrNull(name) as? JsonPrimitive)?.contentOrNull },
216+
{ // an unknown enum value should be coerced to null via decodeNotNullMark if explicitNulls=false :
217+
if (setForceNull(descriptor, index)) return index
218+
}
219+
)
220+
) continue // do not read coerced value
221+
218222
return index
219223
}
220224
}
221225
return CompositeDecoder.DECODE_DONE
222226
}
223227

224-
private fun absenceIsNull(descriptor: SerialDescriptor, index: Int): Boolean {
228+
private fun setForceNull(descriptor: SerialDescriptor, index: Int): Boolean {
225229
forceNull = !json.configuration.explicitNulls
226230
&& !descriptor.isElementOptional(index) && descriptor.getElementDescriptor(index).isNullable
227231
return forceNull
@@ -257,6 +261,8 @@ private open class JsonTreeDecoder(
257261

258262
override fun currentElement(tag: String): JsonElement = value.getValue(tag)
259263

264+
fun currentElementOrNull(tag: String): JsonElement? = value[tag]
265+
260266
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
261267
// polyDiscriminator needs to be preserved so the check for unknown keys
262268
// in endStructure can filter polyDiscriminator out.

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

+17-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,22 @@ private open class DynamicInput(
8585
val name = descriptor.getTag(currentPosition++)
8686
val index = currentPosition - 1
8787
forceNull = false
88-
if ((hasName(name) || absenceIsNull(descriptor, index)) && (!json.configuration.coerceInputValues || !coerceInputValue(descriptor, index, name))) {
88+
89+
if (hasName(name) || setForceNull(descriptor, index)) {
90+
// if forceNull is true, then decodeNotNullMark returns false and `null` is automatically inserted
91+
// by Decoder.decodeIfNullable
92+
if (!json.configuration.coerceInputValues) return index
93+
94+
if (json.tryCoerceValue(
95+
descriptor, index,
96+
{ getByTag(name) == null },
97+
{ getByTag(name) as? String },
98+
{ // an unknown enum value should be coerced to null via decodeNotNullMark if explicitNulls=false :
99+
if (setForceNull(descriptor, index)) return index
100+
}
101+
)
102+
) continue // do not read coerced value
103+
89104
return index
90105
}
91106
}
@@ -94,7 +109,7 @@ private open class DynamicInput(
94109

95110
private fun hasName(name: String) = value[name] !== undefined
96111

97-
private fun absenceIsNull(descriptor: SerialDescriptor, index: Int): Boolean {
112+
private fun setForceNull(descriptor: SerialDescriptor, index: Int): Boolean {
98113
forceNull = !json.configuration.explicitNulls
99114
&& !descriptor.isElementOptional(index) && descriptor.getElementDescriptor(index).isNullable
100115
return forceNull

0 commit comments

Comments
 (0)