Skip to content

Commit 8a99156

Browse files
committed
Disallow missing commas between fields (#2287)
1 parent db217e4 commit 8a99156

File tree

2 files changed

+120
-24
lines changed

2 files changed

+120
-24
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.json
6+
7+
8+
import kotlinx.serialization.*
9+
import kotlinx.serialization.test.*
10+
import kotlin.test.*
11+
12+
class MissingCommaTest: JsonTestBase() {
13+
@Serializable
14+
class Holder(
15+
val i: Int,
16+
val c: Child,
17+
)
18+
19+
@Serializable
20+
class Child(
21+
val i: String
22+
)
23+
24+
private val withTrailingComma = Json { allowTrailingComma = true }
25+
26+
@Test
27+
fun missingCommaBetweenFieldsAfterPrimitive() {
28+
val message =
29+
"Unexpected JSON token at offset 8: Expected comma after the key-value pair at path: \$.i"
30+
val json = """{"i":42 "c":{"i":"string"}}"""
31+
32+
assertFailsWithSerialMessage("JsonDecodingException", message) {
33+
default.decodeFromString<Holder>(json)
34+
}
35+
}
36+
37+
@Test
38+
fun missingCommaBetweenFieldsAfterObject() {
39+
val message =
40+
"Unexpected JSON token at offset 19: Expected comma after the key-value pair at path: \$.c"
41+
val json = """{"c":{"i":"string"}"i":42}"""
42+
43+
assertFailsWithSerialMessage("JsonDecodingException", message) {
44+
default.decodeFromString<Holder>(json)
45+
}
46+
}
47+
48+
@Test
49+
fun allowTrailingCommaDoesNotApplyToCommaBetweenFields() {
50+
val message =
51+
"Unexpected JSON token at offset 8: Expected comma after the key-value pair at path: \$.i"
52+
val json = """{"i":42 "c":{"i":"string"}}"""
53+
54+
assertFailsWithSerialMessage("JsonDecodingException", message) {
55+
withTrailingComma.decodeFromString<Holder>(json)
56+
}
57+
}
58+
59+
@Test
60+
fun lenientSerializeDoesNotAllowToSkipCommaBetweenFields() {
61+
val message =
62+
"Unexpected JSON token at offset 8: Expected comma after the key-value pair at path: \$.i"
63+
val json = """{"i":42 "c":{"i":"string"}}"""
64+
65+
assertFailsWithSerialMessage("JsonDecodingException", message) {
66+
lenient.decodeFromString<Holder>(json)
67+
}
68+
}
69+
70+
@Test
71+
fun missingCommaInDynamicMap(){
72+
val m = "Unexpected JSON token at offset 9: Expected end of the object or comma at path: \$"
73+
val json = """{"i":42 "c":{"i":"string"}}"""
74+
assertFailsWithSerialMessage("JsonDecodingException", m) {
75+
default.parseToJsonElement(json)
76+
}
77+
}
78+
79+
@Test
80+
fun missingCommaInArray(){
81+
val m = "Unexpected JSON token at offset 3: Expected end of the array or comma at path: \$[0]"
82+
val json = """[1 2 3 4]"""
83+
84+
assertFailsWithSerialMessage("JsonDecodingException", m) {
85+
default.decodeFromString<List<Int>>(json)
86+
}
87+
}
88+
}

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

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,8 @@ internal open class StreamingJsonDecoder(
110110
descriptor,
111111
discriminatorHolder
112112
)
113-
else -> if (mode == newMode && json.configuration.explicitNulls) {
114-
this
115-
} else {
116-
StreamingJsonDecoder(json, newMode, lexer, descriptor, discriminatorHolder)
117-
}
113+
114+
else -> StreamingJsonDecoder(json, newMode, lexer, descriptor, discriminatorHolder)
118115
}
119116
}
120117

@@ -220,35 +217,47 @@ internal open class StreamingJsonDecoder(
220217
)
221218

222219
private fun decodeObjectIndex(descriptor: SerialDescriptor): Int {
223-
// hasComma checks are required to properly react on trailing commas
224-
var hasComma = lexer.tryConsumeComma()
225-
while (lexer.canConsumeValue()) { // TODO: consider merging comma consumption and this check
226-
hasComma = false
220+
while (true) {
221+
if (currentIndex != -1) {
222+
val next = lexer.peekNextToken()
223+
if (next == TC_END_OBJ) {
224+
currentIndex = CompositeDecoder.DECODE_DONE
225+
return elementMarker?.nextUnmarkedIndex() ?: CompositeDecoder.DECODE_DONE
226+
}
227+
228+
lexer.require(next == TC_COMMA) { "Expected comma after the key-value pair" }
229+
val commaPosition = lexer.currentPosition
230+
lexer.consumeNextToken()
231+
lexer.require(
232+
configuration.allowTrailingComma || lexer.peekNextToken() != TC_END_OBJ,
233+
position = commaPosition
234+
) { "Trailing comma before the end of JSON object" }
235+
}
236+
237+
if (!lexer.canConsumeValue()) break
238+
227239
val key = decodeStringKey()
228240
lexer.consumeNextToken(COLON)
229241
val index = descriptor.getJsonNameIndex(json, key)
230-
val isUnknown = if (index != UNKNOWN_NAME) {
231-
if (configuration.coerceInputValues && coerceInputValue(descriptor, index)) {
232-
hasComma = lexer.tryConsumeComma()
233-
false // Known element, but coerced
234-
} else {
235-
elementMarker?.mark(index)
236-
return index // Known element without coercing, return it
237-
}
238-
} else {
239-
true // unknown element
242+
243+
if (index == UNKNOWN_NAME) {
244+
handleUnknown(descriptor, key)
245+
currentIndex = UNKNOWN_NAME
246+
continue
240247
}
241248

242-
if (isUnknown) { // slow-path for unknown keys handling
243-
hasComma = handleUnknown(descriptor, key)
249+
if (configuration.coerceInputValues && coerceInputValue(descriptor, index)) {
250+
continue
244251
}
252+
elementMarker?.mark(index)
253+
currentIndex = index
254+
return index
245255
}
246-
if (hasComma && !json.configuration.allowTrailingComma) lexer.invalidTrailingComma()
247256

248257
return elementMarker?.nextUnmarkedIndex() ?: CompositeDecoder.DECODE_DONE
249258
}
250259

251-
private fun handleUnknown(descriptor: SerialDescriptor, key: String): Boolean {
260+
private fun handleUnknown(descriptor: SerialDescriptor, key: String) {
252261
if (descriptor.ignoreUnknownKeys(json) || discriminatorHolder.trySkip(key)) {
253262
lexer.skipElement(configuration.isLenient)
254263
} else {
@@ -257,7 +266,6 @@ internal open class StreamingJsonDecoder(
257266
lexer.path.popDescriptor()
258267
lexer.failOnUnknownKey(key)
259268
}
260-
return lexer.tryConsumeComma()
261269
}
262270

263271
private fun decodeListIndex(): Int {

0 commit comments

Comments
 (0)