Skip to content

Commit fb21578

Browse files
committed
initial attempt at stricter parsing
1 parent bf381a2 commit fb21578

File tree

5 files changed

+42
-21
lines changed

5 files changed

+42
-21
lines changed

formats/json/commonMain/src/kotlinx/serialization/json/Json.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,8 @@ public class JsonBuilder internal constructor(json: Json) {
658658
*/
659659
public var useArrayPolymorphism: Boolean = json.configuration.useArrayPolymorphism
660660

661+
public var allowPrimitiveCoercion: Boolean = json.configuration.allowPrimitiveCoercion
662+
661663
/**
662664
* Module with contextual and polymorphic serializers to be used in the resulting [Json] instance.
663665
*
@@ -695,7 +697,8 @@ public class JsonBuilder internal constructor(json: Json) {
695697
allowStructuredMapKeys, prettyPrint, explicitNulls, prettyPrintIndent,
696698
coerceInputValues, useArrayPolymorphism,
697699
classDiscriminator, allowSpecialFloatingPointValues, useAlternativeNames,
698-
namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma, allowComments, classDiscriminatorMode
700+
namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma, allowComments, classDiscriminatorMode,
701+
allowPrimitiveCoercion
699702
)
700703
}
701704
}

formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public class JsonConfiguration @OptIn(ExperimentalSerializationApi::class) inter
4545
level = DeprecationLevel.ERROR
4646
)
4747
public var classDiscriminatorMode: ClassDiscriminatorMode = ClassDiscriminatorMode.POLYMORPHIC,
48+
@ExperimentalSerializationApi
49+
public val allowPrimitiveCoercion: Boolean = true,
4850
) {
4951

5052
/** @suppress Dokka **/

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

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal open class StreamingJsonDecoder(
2525
descriptor: SerialDescriptor,
2626
discriminatorHolder: DiscriminatorHolder?
2727
) : JsonDecoder, ChunkedDecoder, AbstractDecoder() {
28+
private val coercePrimitives = json.configuration.allowPrimitiveCoercion
2829

2930
// A mutable reference to the discriminator that have to be skipped when in optimistic phase
3031
// of polymorphic serialization, see `decodeSerializableValue`
@@ -273,47 +274,51 @@ internal open class StreamingJsonDecoder(
273274
}
274275

275276
/*
276-
* The primitives are allowed to be quoted and unquoted
277-
* to simplify map key parsing and integrations with third-party API.
278-
*/
277+
* The primitives are allowed to be quoted and unquoted
278+
* to simplify map key parsing and integrations with third-party API.
279+
*/
279280
override fun decodeBoolean(): Boolean {
280-
return lexer.consumeBooleanLenient()
281+
return if (coercePrimitives) {
282+
lexer.consumeBooleanLenient()
283+
} else {
284+
lexer.consumeBoolean()
285+
}
281286
}
282287

283288
override fun decodeByte(): Byte {
284-
val value = lexer.consumeNumericLiteral()
289+
val value = lexer.consumeNumericLiteral(coercePrimitives)
285290
// Check for overflow
286291
if (value != value.toByte().toLong()) lexer.fail("Failed to parse byte for input '$value'")
287292
return value.toByte()
288293
}
289294

290295
override fun decodeShort(): Short {
291-
val value = lexer.consumeNumericLiteral()
296+
val value = lexer.consumeNumericLiteral(coercePrimitives)
292297
// Check for overflow
293298
if (value != value.toShort().toLong()) lexer.fail("Failed to parse short for input '$value'")
294299
return value.toShort()
295300
}
296301

297302
override fun decodeInt(): Int {
298-
val value = lexer.consumeNumericLiteral()
303+
val value = lexer.consumeNumericLiteral(coercePrimitives)
299304
// Check for overflow
300305
if (value != value.toInt().toLong()) lexer.fail("Failed to parse int for input '$value'")
301306
return value.toInt()
302307
}
303308

304309
override fun decodeLong(): Long {
305-
return lexer.consumeNumericLiteral()
310+
return lexer.consumeNumericLiteral(coercePrimitives)
306311
}
307312

308313
override fun decodeFloat(): Float {
309-
val result = lexer.parseString("float") { toFloat() }
314+
val result = lexer.parseString("float", coercePrimitives) { toFloat() }
310315
val specialFp = json.configuration.allowSpecialFloatingPointValues
311316
if (specialFp || result.isFinite()) return result
312317
lexer.throwInvalidFloatingPointDecoded(result)
313318
}
314319

315320
override fun decodeDouble(): Double {
316-
val result = lexer.parseString("double") { toDouble() }
321+
val result = lexer.parseString("double", coercePrimitives) { toDouble() }
317322
val specialFp = json.configuration.allowSpecialFloatingPointValues
318323
if (specialFp || result.isFinite()) return result
319324
lexer.throwInvalidFloatingPointDecoded(result)
@@ -374,15 +379,16 @@ internal class JsonDecoderForUnsignedTypes(
374379
) : AbstractDecoder() {
375380
override val serializersModule: SerializersModule = json.serializersModule
376381
override fun decodeElementIndex(descriptor: SerialDescriptor): Int = error("unsupported")
382+
private val coercePrimitves = json.configuration.allowPrimitiveCoercion
377383

378-
override fun decodeInt(): Int = lexer.parseString("UInt") { toUInt().toInt() }
379-
override fun decodeLong(): Long = lexer.parseString("ULong") { toULong().toLong() }
380-
override fun decodeByte(): Byte = lexer.parseString("UByte") { toUByte().toByte() }
381-
override fun decodeShort(): Short = lexer.parseString("UShort") { toUShort().toShort() }
384+
override fun decodeInt(): Int = lexer.parseString("UInt", coercePrimitves) { toUInt().toInt() }
385+
override fun decodeLong(): Long = lexer.parseString("ULong", coercePrimitves) { toULong().toLong() }
386+
override fun decodeByte(): Byte = lexer.parseString("UByte", coercePrimitves) { toUByte().toByte() }
387+
override fun decodeShort(): Short = lexer.parseString("UShort", coercePrimitves) { toUShort().toShort() }
382388
}
383389

384-
private inline fun <T> AbstractJsonLexer.parseString(expectedType: String, block: String.() -> T): T {
385-
val input = consumeStringLenient()
390+
private inline fun <T> AbstractJsonLexer.parseString(expectedType: String, coercePrimitives: Boolean, block: String.() -> T): T {
391+
val input = consumeOther(allowQuoted = coercePrimitives)
386392
try {
387393
return input.block()
388394
} catch (e: IllegalArgumentException) {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ private sealed class AbstractJsonTreeDecoder(
8585

8686
private inline fun <T : Any> getPrimitiveValue(tag: String, primitiveName: String, convert: JsonPrimitive.() -> T?): T {
8787
val literal = cast<JsonPrimitive>(currentElement(tag), primitiveName, tag)
88+
if (!json.configuration.allowPrimitiveCoercion && literal.isString) {
89+
unparsedPrimitive(literal, primitiveName, tag)
90+
}
8891
try {
8992
return literal.convert() ?: unparsedPrimitive(literal, primitiveName, tag)
9093
} catch (e: IllegalArgumentException) {

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,14 +438,18 @@ internal abstract class AbstractJsonLexer {
438438
}
439439

440440
// Allows consuming unquoted string
441-
fun consumeStringLenient(): String {
441+
fun consumeStringLenient(): String =
442+
consumeOther(allowQuoted = true)
443+
444+
fun consumeOther(allowQuoted: Boolean): String {
445+
// TODO this string peeking stuff might break things...
442446
if (peekedString != null) {
443447
return takePeeked()
444448
}
445449
var current = skipWhitespaces()
446450
if (current >= source.length || current == -1) fail("EOF", current)
447451
val token = charToTokenClass(source[current])
448-
if (token == TC_STRING) {
452+
if (allowQuoted && token == TC_STRING) {
449453
return consumeString()
450454
}
451455

@@ -587,15 +591,18 @@ internal abstract class AbstractJsonLexer {
587591
throw JsonDecodingException(position, message + " at path: " + path.getPath() + hintMessage, source)
588592
}
589593

590-
fun consumeNumericLiteral(): Long {
594+
fun consumeNumericLiteral(): Long =
595+
consumeNumericLiteral(coercePrimitives = true)
596+
597+
fun consumeNumericLiteral(coercePrimitives: Boolean): Long {
591598
/*
592599
* This is an optimized (~40% for numbers) version of consumeString().toLong()
593600
* that doesn't allocate and also doesn't support any radix but 10
594601
*/
595602
var current = skipWhitespaces()
596603
current = prefetchOrEof(current)
597604
if (current >= source.length || current == -1) fail("EOF")
598-
val hasQuotation = if (source[current] == STRING) {
605+
val hasQuotation = if (coercePrimitives && source[current] == STRING) {
599606
// Check it again
600607
// not sure if should call ensureHaveChars() because threshold is far greater than chars count in MAX_LONG
601608
if (++current == source.length) fail("EOF")

0 commit comments

Comments
 (0)