Skip to content

Commit 3191884

Browse files
authored
Fix error triggered by 'consume leading class discriminator' polymorphic parsing optimization: (#2362)
In case of an empty object, no exception should be thrown, it should be treated as missing class discriminator. peekString() correctly handles this case. Also fixes misplaced calls in lenient vs non-lenient mode. Fixes #2353
1 parent b8de86f commit 3191884

File tree

5 files changed

+54
-13
lines changed

5 files changed

+54
-13
lines changed

formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,42 @@ class PolymorphismTest : JsonTestBase() {
143143
val s = json.encodeToString(Wrapper.serializer(), obj, jsonTestingMode)
144144
assertEquals("""{"polyBase1":{"type":"even","parity":"even"},"polyBase2":{"type":"odd","parity":"odd"}}""", s)
145145
}
146+
147+
@Serializable
148+
sealed class Conf {
149+
@Serializable
150+
@SerialName("empty")
151+
object Empty : Conf() // default
152+
153+
@Serializable
154+
@SerialName("simple")
155+
data class Simple(val value: String) : Conf()
156+
}
157+
158+
private val jsonForConf = Json {
159+
isLenient = false
160+
ignoreUnknownKeys = true
161+
serializersModule = SerializersModule {
162+
polymorphicDefaultDeserializer(Conf::class) { Conf.Empty.serializer() }
163+
}
164+
}
165+
166+
@Test
167+
fun defaultSerializerWithEmptyBodyTest() = parametrizedTest { mode ->
168+
assertEquals(Conf.Simple("123"), jsonForConf.decodeFromString<Conf>("""{"type": "simple", "value": "123"}""", mode))
169+
assertEquals(Conf.Empty, jsonForConf.decodeFromString<Conf>("""{"type": "default"}""", mode))
170+
assertEquals(Conf.Empty, jsonForConf.decodeFromString<Conf>("""{"unknown": "Meow"}""", mode))
171+
assertEquals(Conf.Empty, jsonForConf.decodeFromString<Conf>("""{}""", mode))
172+
}
173+
174+
@Test
175+
fun testTypeKeysInLenientMode() = parametrizedTest { mode ->
176+
val json = Json(jsonForConf) { isLenient = true }
177+
178+
assertEquals(Conf.Simple("123"), json.decodeFromString<Conf>("""{type: simple, value: 123}""", mode))
179+
assertEquals(Conf.Empty, json.decodeFromString<Conf>("""{type: default}""", mode))
180+
assertEquals(Conf.Empty, json.decodeFromString<Conf>("""{unknown: Meow}""", mode))
181+
assertEquals(Conf.Empty, json.decodeFromString<Conf>("""{}""", mode))
182+
183+
}
146184
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ internal open class StreamingJsonDecoder(
7171
}
7272

7373
val discriminator = deserializer.descriptor.classDiscriminator(json)
74-
val type = lexer.consumeLeadingMatchingValue(discriminator, configuration.isLenient)
74+
val type = lexer.peekLeadingMatchingValue(discriminator, configuration.isLenient)
7575
var actualSerializer: DeserializationStrategy<Any>? = null
7676
if (type != null) {
7777
actualSerializer = deserializer.findPolymorphicSerializerOrNull(this, type)

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ internal abstract class AbstractJsonLexer {
286286
return current
287287
}
288288

289-
abstract fun consumeLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String?
289+
abstract fun peekLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String?
290290

291291
fun peekString(isLenient: Boolean): String? {
292292
val token = peekNextToken()
@@ -301,6 +301,10 @@ internal abstract class AbstractJsonLexer {
301301
return string
302302
}
303303

304+
fun discardPeeked() {
305+
peekedString = null
306+
}
307+
304308
open fun indexOf(char: Char, startPos: Int) = source.indexOf(char, startPos)
305309
open fun substring(startPos: Int, endPos: Int) = source.substring(startPos, endPos)
306310

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ internal class ReaderJsonLexer(
179179
}
180180

181181
// Can be carefully implemented but postponed for now
182-
override fun consumeLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String? = null
182+
override fun peekLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String? = null
183183

184184
fun release() {
185185
CharArrayPoolBatchSize.release(buffer)

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,20 @@ internal class StringJsonLexer(override val source: String) : AbstractJsonLexer(
101101
(if (isLenient) consumeStringLenient() else consumeString()).chunked(BATCH_SIZE).forEach(consumeChunk)
102102
}
103103

104-
override fun consumeLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String? {
104+
override fun peekLeadingMatchingValue(keyToMatch: String, isLenient: Boolean): String? {
105105
val positionSnapshot = currentPosition
106106
try {
107-
// Malformed JSON, bailout
108-
if (consumeNextToken() != TC_BEGIN_OBJ) return null
109-
val firstKey = if (isLenient) consumeKeyString() else consumeStringLenientNotNull()
110-
if (firstKey == keyToMatch) {
111-
if (consumeNextToken() != TC_COLON) return null
112-
val result = if (isLenient) consumeString() else consumeStringLenientNotNull()
113-
return result
114-
}
115-
return null
107+
if (consumeNextToken() != TC_BEGIN_OBJ) return null // Malformed JSON, bailout
108+
val firstKey = peekString(isLenient)
109+
if (firstKey != keyToMatch) return null
110+
discardPeeked() // consume firstKey
111+
if (consumeNextToken() != TC_COLON) return null
112+
return peekString(isLenient)
116113
} finally {
117114
// Restore the position
118115
currentPosition = positionSnapshot
116+
discardPeeked()
119117
}
120118
}
121119
}
120+

0 commit comments

Comments
 (0)