summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Shanshin <sergey.shanshin@jetbrains.com>2023-02-06 17:42:42 +0300
committerGitHub <noreply@github.com>2023-02-06 17:42:42 +0300
commit2cb7f7dce26764a751b5a63b20eb359a44183ba7 (patch)
tree59b252281976890c1490ef1ccc70be89704356ee
parentb454f34b5991d25d46c3be3c402933bd297e7db2 (diff)
downloadkotlinx.serialization-2cb7f7dce26764a751b5a63b20eb359a44183ba7.tar.gz
Added support for null values for nullable enums in lanient mode (#2176)
Fixed #2170 Co-authored-by: Leonid Startsev <sandwwraith@users.noreply.github.com>
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt15
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt8
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt4
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt19
4 files changed, 33 insertions, 13 deletions
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 fd8d516c..ecb946cb 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt
@@ -5,7 +5,6 @@
package kotlinx.serialization.json
import kotlinx.serialization.*
-import kotlinx.serialization.json.internal.*
import kotlinx.serialization.test.assertFailsWithSerial
import kotlin.test.*
@@ -25,6 +24,11 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
val foo: String
)
+ @Serializable
+ data class NullableEnumHolder(
+ val enum: SampleEnum?
+ )
+
val json = Json {
coerceInputValues = true
isLenient = true
@@ -99,4 +103,13 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
assertEquals(expected, json.decodeFromString(MultipleValues.serializer(), input), "Failed on input: $input")
}
}
+
+ @Test
+ fun testNullSupportForEnums() = parametrizedTest(json) {
+ var decoded = decodeFromString<NullableEnumHolder>("""{"enum": null}""")
+ assertNull(decoded.enum)
+
+ decoded = decodeFromString<NullableEnumHolder>("""{"enum": OptionA}""")
+ assertEquals(SampleEnum.OptionA, decoded.enum)
+ }
}
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 bf616f98..762bacd9 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
@@ -98,12 +98,16 @@ internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String,
@OptIn(ExperimentalSerializationApi::class)
internal inline fun Json.tryCoerceValue(
elementDescriptor: SerialDescriptor,
- peekNull: () -> Boolean,
+ peekNull: (consume: Boolean) -> Boolean,
peekString: () -> String?,
onEnumCoercing: () -> Unit = {}
): Boolean {
- if (!elementDescriptor.isNullable && peekNull()) return true
+ if (!elementDescriptor.isNullable && peekNull(true)) return true
if (elementDescriptor.kind == SerialKind.ENUM) {
+ if (elementDescriptor.isNullable && peekNull(false)) {
+ return false
+ }
+
val enumValue = peekString()
?: return false // if value is not a string, decodeEnum() will throw correct exception
val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue)
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 c7648ad6..627ee7bb 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
@@ -133,7 +133,7 @@ internal open class StreamingJsonDecoder(
}
override fun decodeNotNullMark(): Boolean {
- return !(elementMarker?.isUnmarkedNull ?: false) && lexer.tryConsumeNotNull()
+ return !(elementMarker?.isUnmarkedNull ?: false) && !lexer.tryConsumeNull()
}
override fun decodeNull(): Nothing? {
@@ -208,7 +208,7 @@ internal open class StreamingJsonDecoder(
*/
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int): Boolean = json.tryCoerceValue(
descriptor.getElementDescriptor(index),
- { !lexer.tryConsumeNotNull() },
+ { lexer.tryConsumeNull(it) },
{ lexer.peekString(configuration.isLenient) },
{ lexer.consumeString() /* skip unknown enum string*/ }
)
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 977347a5..ce81f199 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
@@ -244,25 +244,28 @@ internal abstract class AbstractJsonLexer {
/**
* Tries to consume `null` token from input.
- * Returns `true` if the next 4 chars in input are not `null`,
- * `false` otherwise and consumes it.
+ * Returns `false` if the next 4 chars in input are not `null`,
+ * `true` otherwise and consumes it if [doConsume] is `true`.
*/
- fun tryConsumeNotNull(): Boolean {
+ fun tryConsumeNull(doConsume: Boolean = true): Boolean {
var current = skipWhitespaces()
current = prefetchOrEof(current)
// Cannot consume null due to EOF, maybe something else
val len = source.length - current
- if (len < 4 || current == -1) return true
+ if (len < 4 || current == -1) return false
for (i in 0..3) {
- if (NULL[i] != source[current + i]) return true
+ if (NULL[i] != source[current + i]) return false
}
/*
* If we're in lenient mode, this might be the string with 'null' prefix,
* distinguish it from 'null'
*/
- if (len > 4 && charToTokenClass(source[current + 4]) == TC_OTHER) return true
- currentPosition = current + 4
- return false
+ if (len > 4 && charToTokenClass(source[current + 4]) == TC_OTHER) return false
+
+ if (doConsume) {
+ currentPosition = current + 4
+ }
+ return true
}
open fun skipWhitespaces(): Int {