Skip to content

Commit

Permalink
Support decoding maps with boolean keys (#2440)
Browse files Browse the repository at this point in the history
We ignore quoted/unquoted state when decoding maps with number keys, so it is logical to do the same for boolean maps.

Fixes #2438
  • Loading branch information
sandwwraith authored Sep 14, 2023
1 parent 01fcfee commit 7d287c8
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.serialization.json

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
Expand Down Expand Up @@ -41,6 +42,9 @@ class JsonMapKeysTest : JsonTestBase() {
@Serializable
private data class WithMap(val map: Map<Long, Long>)

@Serializable
private data class WithBooleanMap(val map: Map<Boolean, Boolean>)

@Serializable
private data class WithValueKeyMap(val map: Map<PrimitiveCarrier, Long>)

Expand All @@ -60,13 +64,42 @@ class JsonMapKeysTest : JsonTestBase() {
private data class WithContextualKey(val map: Map<@Contextual ContextualValue, Long>)

@Test
fun testMapKeysShouldBeStrings() = parametrizedTest(default) {
fun testMapKeysSupportNumbers() = parametrizedTest {
assertStringFormAndRestored(
"""{"map":{"10":10,"20":20}}""",
WithMap(mapOf(10L to 10L, 20L to 20L)),
WithMap.serializer(),
this
default
)
}

@Test
fun testMapKeysSupportBooleans() = parametrizedTest {
assertStringFormAndRestored(
"""{"map":{"true":false,"false":true}}""",
WithBooleanMap(mapOf(true to false, false to true)),
WithBooleanMap.serializer(),
default
)
}

// As a result of quoting ignorance when parsing primitives, it is possible to parse unquoted maps if Kotlin keys are non-string primitives.
// This is not spec-compliant, but I do not see any problems with it.
@Test
fun testMapDeserializesUnquotedKeys() = parametrizedTest {
assertEquals(WithMap(mapOf(10L to 10L, 20L to 20L)), default.decodeFromString("""{"map":{10:10,20:20}}"""))
assertEquals(
WithBooleanMap(mapOf(true to false, false to true)),
default.decodeFromString("""{"map":{true:false,false:true}}""")
)
assertFailsWithSerial("JsonDecodingException") {
default.decodeFromString(
MapSerializer(
String.serializer(),
Boolean.serializer()
),"""{"map":{true:false,false:true}}"""
)
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class LenientTest : JsonTestBase() {
@Test
fun testQuotedBoolean() = parametrizedTest {
val json = """{"i":1, "l":2, "b":"true", "s":"string"}"""
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString(Holder.serializer(), json, it) }
assertEquals(value, default.decodeFromString(Holder.serializer(), json, it))
assertEquals(value, lenient.decodeFromString(Holder.serializer(), json, it))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,23 +268,14 @@ internal open class StreamingJsonDecoder(
}
}


/*
* The primitives are allowed to be quoted and unquoted
* to simplify map key parsing and integrations with third-party API.
*/
override fun decodeBoolean(): Boolean {
/*
* We prohibit any boolean literal that is not strictly 'true' or 'false' as it is considered way too
* error-prone, but allow quoted literal in relaxed mode for booleans.
*/
return if (configuration.isLenient) {
lexer.consumeBooleanLenient()
} else {
lexer.consumeBoolean()
}
return lexer.consumeBooleanLenient()
}

/*
* The rest of the primitives are allowed to be quoted and unquoted
* to simplify integrations with third-party API.
*/
override fun decodeByte(): Byte {
val value = lexer.consumeNumericLiteral()
// Check for overflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,7 @@ private sealed class AbstractJsonTreeDecoder(
override fun decodeTaggedNotNullMark(tag: String): Boolean = currentElement(tag) !== JsonNull

override fun decodeTaggedBoolean(tag: String): Boolean {
val value = getPrimitiveValue(tag)
if (!json.configuration.isLenient) {
val literal = value.asLiteral("boolean")
if (literal.isString) throw JsonDecodingException(
-1, "Boolean literal for key '$tag' should be unquoted.\n$lenientHint", currentObject().toString()
)
}
return value.primitive("boolean") {
booleanOrNull ?: throw IllegalArgumentException() /* Will be handled by 'primitive' */
}
return getPrimitiveValue(tag).primitive("boolean", JsonPrimitive::booleanOrNull)
}

override fun decodeTaggedByte(tag: String) = getPrimitiveValue(tag).primitive("byte") {
Expand Down

0 comments on commit 7d287c8

Please sign in to comment.