Skip to content

Commit 7d287c8

Browse files
authored
Support decoding maps with boolean keys (#2440)
We ignore quoted/unquoted state when decoding maps with number keys, so it is logical to do the same for boolean maps. Fixes #2438
1 parent 01fcfee commit 7d287c8

File tree

4 files changed

+42
-27
lines changed

4 files changed

+42
-27
lines changed

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.serialization.json
66

77
import kotlinx.serialization.*
8+
import kotlinx.serialization.builtins.*
89
import kotlinx.serialization.descriptors.PrimitiveKind
910
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
1011
import kotlinx.serialization.descriptors.SerialDescriptor
@@ -41,6 +42,9 @@ class JsonMapKeysTest : JsonTestBase() {
4142
@Serializable
4243
private data class WithMap(val map: Map<Long, Long>)
4344

45+
@Serializable
46+
private data class WithBooleanMap(val map: Map<Boolean, Boolean>)
47+
4448
@Serializable
4549
private data class WithValueKeyMap(val map: Map<PrimitiveCarrier, Long>)
4650

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

6266
@Test
63-
fun testMapKeysShouldBeStrings() = parametrizedTest(default) {
67+
fun testMapKeysSupportNumbers() = parametrizedTest {
6468
assertStringFormAndRestored(
6569
"""{"map":{"10":10,"20":20}}""",
6670
WithMap(mapOf(10L to 10L, 20L to 20L)),
6771
WithMap.serializer(),
68-
this
72+
default
73+
)
74+
}
75+
76+
@Test
77+
fun testMapKeysSupportBooleans() = parametrizedTest {
78+
assertStringFormAndRestored(
79+
"""{"map":{"true":false,"false":true}}""",
80+
WithBooleanMap(mapOf(true to false, false to true)),
81+
WithBooleanMap.serializer(),
82+
default
83+
)
84+
}
85+
86+
// As a result of quoting ignorance when parsing primitives, it is possible to parse unquoted maps if Kotlin keys are non-string primitives.
87+
// This is not spec-compliant, but I do not see any problems with it.
88+
@Test
89+
fun testMapDeserializesUnquotedKeys() = parametrizedTest {
90+
assertEquals(WithMap(mapOf(10L to 10L, 20L to 20L)), default.decodeFromString("""{"map":{10:10,20:20}}"""))
91+
assertEquals(
92+
WithBooleanMap(mapOf(true to false, false to true)),
93+
default.decodeFromString("""{"map":{true:false,false:true}}""")
6994
)
95+
assertFailsWithSerial("JsonDecodingException") {
96+
default.decodeFromString(
97+
MapSerializer(
98+
String.serializer(),
99+
Boolean.serializer()
100+
),"""{"map":{true:false,false:true}}"""
101+
)
102+
}
70103
}
71104

72105
@Test

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class LenientTest : JsonTestBase() {
3737
@Test
3838
fun testQuotedBoolean() = parametrizedTest {
3939
val json = """{"i":1, "l":2, "b":"true", "s":"string"}"""
40-
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString(Holder.serializer(), json, it) }
40+
assertEquals(value, default.decodeFromString(Holder.serializer(), json, it))
4141
assertEquals(value, lenient.decodeFromString(Holder.serializer(), json, it))
4242
}
4343

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

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -268,23 +268,14 @@ internal open class StreamingJsonDecoder(
268268
}
269269
}
270270

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

284-
/*
285-
* The rest of the primitives are allowed to be quoted and unquoted
286-
* to simplify integrations with third-party API.
287-
*/
288279
override fun decodeByte(): Byte {
289280
val value = lexer.consumeNumericLiteral()
290281
// Check for overflow

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,7 @@ private sealed class AbstractJsonTreeDecoder(
9191
override fun decodeTaggedNotNullMark(tag: String): Boolean = currentElement(tag) !== JsonNull
9292

9393
override fun decodeTaggedBoolean(tag: String): Boolean {
94-
val value = getPrimitiveValue(tag)
95-
if (!json.configuration.isLenient) {
96-
val literal = value.asLiteral("boolean")
97-
if (literal.isString) throw JsonDecodingException(
98-
-1, "Boolean literal for key '$tag' should be unquoted.\n$lenientHint", currentObject().toString()
99-
)
100-
}
101-
return value.primitive("boolean") {
102-
booleanOrNull ?: throw IllegalArgumentException() /* Will be handled by 'primitive' */
103-
}
94+
return getPrimitiveValue(tag).primitive("boolean", JsonPrimitive::booleanOrNull)
10495
}
10596

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

0 commit comments

Comments
 (0)