Skip to content

Commit 2cb7f7d

Browse files
Added support for null values for nullable enums in lanient mode (#2176)
Fixed #2170 Co-authored-by: Leonid Startsev <sandwwraith@users.noreply.github.com>
1 parent b454f34 commit 2cb7f7d

File tree

4 files changed

+33
-13
lines changed

4 files changed

+33
-13
lines changed

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package kotlinx.serialization.json
66

77
import kotlinx.serialization.*
8-
import kotlinx.serialization.json.internal.*
98
import kotlinx.serialization.test.assertFailsWithSerial
109
import kotlin.test.*
1110

@@ -25,6 +24,11 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
2524
val foo: String
2625
)
2726

27+
@Serializable
28+
data class NullableEnumHolder(
29+
val enum: SampleEnum?
30+
)
31+
2832
val json = Json {
2933
coerceInputValues = true
3034
isLenient = true
@@ -99,4 +103,13 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
99103
assertEquals(expected, json.decodeFromString(MultipleValues.serializer(), input), "Failed on input: $input")
100104
}
101105
}
106+
107+
@Test
108+
fun testNullSupportForEnums() = parametrizedTest(json) {
109+
var decoded = decodeFromString<NullableEnumHolder>("""{"enum": null}""")
110+
assertNull(decoded.enum)
111+
112+
decoded = decodeFromString<NullableEnumHolder>("""{"enum": OptionA}""")
113+
assertEquals(SampleEnum.OptionA, decoded.enum)
114+
}
102115
}

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,16 @@ internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String,
9898
@OptIn(ExperimentalSerializationApi::class)
9999
internal inline fun Json.tryCoerceValue(
100100
elementDescriptor: SerialDescriptor,
101-
peekNull: () -> Boolean,
101+
peekNull: (consume: Boolean) -> Boolean,
102102
peekString: () -> String?,
103103
onEnumCoercing: () -> Unit = {}
104104
): Boolean {
105-
if (!elementDescriptor.isNullable && peekNull()) return true
105+
if (!elementDescriptor.isNullable && peekNull(true)) return true
106106
if (elementDescriptor.kind == SerialKind.ENUM) {
107+
if (elementDescriptor.isNullable && peekNull(false)) {
108+
return false
109+
}
110+
107111
val enumValue = peekString()
108112
?: return false // if value is not a string, decodeEnum() will throw correct exception
109113
val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue)

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ internal open class StreamingJsonDecoder(
133133
}
134134

135135
override fun decodeNotNullMark(): Boolean {
136-
return !(elementMarker?.isUnmarkedNull ?: false) && lexer.tryConsumeNotNull()
136+
return !(elementMarker?.isUnmarkedNull ?: false) && !lexer.tryConsumeNull()
137137
}
138138

139139
override fun decodeNull(): Nothing? {
@@ -208,7 +208,7 @@ internal open class StreamingJsonDecoder(
208208
*/
209209
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int): Boolean = json.tryCoerceValue(
210210
descriptor.getElementDescriptor(index),
211-
{ !lexer.tryConsumeNotNull() },
211+
{ lexer.tryConsumeNull(it) },
212212
{ lexer.peekString(configuration.isLenient) },
213213
{ lexer.consumeString() /* skip unknown enum string*/ }
214214
)

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -244,25 +244,28 @@ internal abstract class AbstractJsonLexer {
244244

245245
/**
246246
* Tries to consume `null` token from input.
247-
* Returns `true` if the next 4 chars in input are not `null`,
248-
* `false` otherwise and consumes it.
247+
* Returns `false` if the next 4 chars in input are not `null`,
248+
* `true` otherwise and consumes it if [doConsume] is `true`.
249249
*/
250-
fun tryConsumeNotNull(): Boolean {
250+
fun tryConsumeNull(doConsume: Boolean = true): Boolean {
251251
var current = skipWhitespaces()
252252
current = prefetchOrEof(current)
253253
// Cannot consume null due to EOF, maybe something else
254254
val len = source.length - current
255-
if (len < 4 || current == -1) return true
255+
if (len < 4 || current == -1) return false
256256
for (i in 0..3) {
257-
if (NULL[i] != source[current + i]) return true
257+
if (NULL[i] != source[current + i]) return false
258258
}
259259
/*
260260
* If we're in lenient mode, this might be the string with 'null' prefix,
261261
* distinguish it from 'null'
262262
*/
263-
if (len > 4 && charToTokenClass(source[current + 4]) == TC_OTHER) return true
264-
currentPosition = current + 4
265-
return false
263+
if (len > 4 && charToTokenClass(source[current + 4]) == TC_OTHER) return false
264+
265+
if (doConsume) {
266+
currentPosition = current + 4
267+
}
268+
return true
266269
}
267270

268271
open fun skipWhitespaces(): Int {

0 commit comments

Comments
 (0)