Skip to content

Commit 1fecb99

Browse files
committed
Ensure that serialization exception is thrown from JSON parser on invalid inputs
Fixes #704
1 parent 9ed6235 commit 1fecb99

File tree

9 files changed

+96
-60
lines changed

9 files changed

+96
-60
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import kotlin.jvm.*
1717
* * [ignoreUnknownKeys] ignores encounters of unknown properties in the input JSON.
1818
*
1919
* * [isLenient] removes JSON specification restriction (RFC-4627) and makes parser
20-
* more liberal to the malformed input. In lenient modes quoted string literals,
20+
* more liberal to the malformed input. In lenient mode quoted boolean literals,
2121
* and unquoted string literals are allowed.
2222
*
2323
* * [serializeSpecialFloatingPointValues] removes JSON specification restriction on
2424
* special floating-point values such as `NaN` and `Infinity` and enables their
25-
* serialization. When enabling it, please assure that the receiving party will be
26-
* able to parse these special value.
25+
* serialization. When enabling it, please ensure that the receiving party will be
26+
* able to parse these special values.
2727
*
2828
* * [unquotedPrint] specifies whether keys and values should be quoted when building the
2929
* JSON string. This option is intended to be used for debugging and pretty-printing,
@@ -114,6 +114,6 @@ public data class JsonConfiguration @UnstableDefault constructor(
114114
"'ignoreUnknownKeys', 'isLenient' and 'serializeSpecialFloatingPointValues'")
115115
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
116116
@kotlin.internal.LowPriorityInOverloadResolution
117-
fun JsonConfiguration(strictMode: Boolean = true, unquoted: Boolean = false) {
117+
public fun JsonConfiguration(strictMode: Boolean = true, unquoted: Boolean = false) {
118118
error("Should not be called")
119119
}

runtime/commonMain/src/kotlinx/serialization/json/JsonElementSerializer.kt

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ public object JsonElementSerializer : KSerializer<JsonElement> {
4141
}
4242

4343
override fun deserialize(decoder: Decoder): JsonElement {
44-
verify(decoder)
45-
val input = decoder as JsonInput
44+
val input = decoder.asJsonInput()
4645
return input.decodeJson()
4746
}
4847
}
@@ -66,11 +65,8 @@ public object JsonPrimitiveSerializer : KSerializer<JsonPrimitive> {
6665
}
6766

6867
override fun deserialize(decoder: Decoder): JsonPrimitive {
69-
verify(decoder)
70-
val result = (decoder as JsonInput).decodeJson()
71-
require(result is JsonPrimitive) {
72-
"Unexpected JsonElement type: " + result::class
73-
}
68+
val result = decoder.asJsonInput().decodeJson()
69+
if (result !is JsonPrimitive) throw JsonDecodingException(-1, "Unexpected JSON element, expected JsonPrimitive, had ${result::class}", result.toString())
7470
return result
7571
}
7672
}
@@ -131,11 +127,8 @@ public object JsonLiteralSerializer : KSerializer<JsonLiteral> {
131127
}
132128

133129
override fun deserialize(decoder: Decoder): JsonLiteral {
134-
verify(decoder)
135-
val result = (decoder as JsonInput).decodeJson()
136-
require(result is JsonLiteral) {
137-
"Unexpected JsonElement type: " + result::class
138-
}
130+
val result = decoder.asJsonInput().decodeJson()
131+
if (result !is JsonLiteral) throw JsonDecodingException(-1, "Unexpected JSON element, expected JsonLiteral, had ${result::class}", result.toString())
139132
return result
140133
}
141134
}
@@ -188,13 +181,21 @@ public object JsonArraySerializer : KSerializer<JsonArray> {
188181
}
189182

190183
private fun verify(encoder: Encoder) {
191-
require(encoder is JsonOutput) {
192-
"Json element serializer can be used only by Json format, had $encoder"
193-
}
184+
encoder.asJsonOutput()
194185
}
195186

196187
private fun verify(decoder: Decoder) {
197-
require(decoder is JsonInput) {
198-
"Json element serializer can be used only by Json format, had $decoder"
199-
}
188+
decoder.asJsonInput()
200189
}
190+
191+
internal fun Decoder.asJsonInput(): JsonInput = this as? JsonInput
192+
?: throw IllegalStateException(
193+
"This serializer can be used only with Json format." +
194+
"Expected Decoder to be JsonInput, got ${this::class}"
195+
)
196+
197+
internal fun Encoder.asJsonOutput() = this as? JsonOutput
198+
?: throw IllegalStateException(
199+
"This serializer can be used only with Json format." +
200+
"Expected Encoder to be JsonOutput, got ${this::class}"
201+
)

runtime/commonMain/src/kotlinx/serialization/json/JsonExceptions.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package kotlinx.serialization.json
77
import kotlinx.serialization.*
88

99
/**
10-
* Generic exception indicating a problem with JSON serialization.
10+
* Generic exception indicating a problem with JSON serialization and deserialization.
1111
*/
1212
public open class JsonException(message: String) : SerializationException(message)
1313

@@ -39,7 +39,9 @@ internal fun InvalidFloatingPoint(value: Number, key: String, type: String, outp
3939

4040
internal fun UnknownKeyException(key: String, input: String) = JsonDecodingException(
4141
-1,
42-
"JSON encountered unknown key: '$key'. You can enable 'ignoreUnknownKeys' property to ignore unknown keys. JSON input: $input")
42+
"JSON encountered unknown key: '$key'. You can enable 'JsonConfiguration.ignoreUnknownKeys' property to ignore unknown keys.\n" +
43+
" JSON input: ${input.minify()}"
44+
)
4345

4446
internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEncodingException(
4547
"Value of type '${keyDescriptor.serialName}' can't be used in JSON as a key in the map. " +
@@ -48,7 +50,7 @@ internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEnco
4850
)
4951

5052
private fun String.minify(offset: Int = -1): String {
51-
if (offset < 200) return this
53+
if (length < 200) return this
5254
if (offset == -1) {
5355
val start = this.length - 60
5456
if (start <= 0) return this

runtime/commonMain/src/kotlinx/serialization/json/JsonInput.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,3 @@ public interface JsonInput : Decoder, CompositeDecoder {
5151
*/
5252
public fun decodeJson(): JsonElement
5353
}
54-
55-
internal fun Decoder.asJsonInput() = this as? JsonInput
56-
?: throw SerializationException(
57-
"This serializer can be used only with Json format." +
58-
"Expected Decoder to be JsonInput, got ${this::class}"
59-
)

runtime/commonMain/src/kotlinx/serialization/json/JsonOutput.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,3 @@ public interface JsonOutput: Encoder, CompositeEncoder {
5151
*/
5252
public fun encodeJson(element: JsonElement)
5353
}
54-
55-
internal fun Encoder.asJsonOutput() = this as? JsonOutput
56-
?: throw SerializationException(
57-
"This serializer can be used only with Json format." +
58-
"Expected Encoder to be JsonOutput, got ${this::class}"
59-
)

runtime/commonMain/src/kotlinx/serialization/json/internal/JsonReader.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,14 @@ internal class JsonReader(private val source: String) {
225225
length = 0 // in buffer
226226
var currentPosition = startPosition + 1
227227
var lastPosition = currentPosition
228-
val length = source.length
229228
while (source[currentPosition] != STRING) {
230-
if (currentPosition >= length) fail("Unexpected EOF", currentPosition)
231229
if (source[currentPosition] == STRING_ESC) {
232230
appendRange(source, lastPosition, currentPosition)
233231
val newPosition = appendEsc(source, currentPosition + 1)
234232
currentPosition = newPosition
235233
lastPosition = newPosition
236-
} else {
237-
currentPosition++
234+
} else if (++currentPosition >= source.length) {
235+
fail("EOF", currentPosition)
238236
}
239237
}
240238
if (lastPosition == startPosition + 1) {

runtime/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonInput.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ internal class StreamingJsonInput internal constructor(
121121

122122
if (!configuration.ignoreUnknownKeys) {
123123
reader.fail(
124-
"Encountered an unknown key '$key'. You can enable 'ignoreUnknownKeys' property" +
124+
"Encountered an unknown key '$key'. You can enable 'JsonConfiguration.ignoreUnknownKeys' property" +
125125
" to ignore unknown keys"
126126
)
127127
} else {
@@ -165,13 +165,14 @@ internal class StreamingJsonInput internal constructor(
165165
* The rest of the primitives are allowed to be quoted and unqouted
166166
* to simplify integrations with third-party API.
167167
*/
168-
override fun decodeByte(): Byte = reader.takeString().toByte()
169-
override fun decodeShort(): Short = reader.takeString().toShort()
170-
override fun decodeInt(): Int = reader.takeString().toInt()
171-
override fun decodeLong(): Long = reader.takeString().toLong()
172-
override fun decodeFloat(): Float = reader.takeString().toFloat()
173-
override fun decodeDouble(): Double = reader.takeString().toDouble()
174-
override fun decodeChar(): Char = reader.takeString().single()
168+
override fun decodeByte(): Byte = reader.takeString().parse("byte") { toByte() }
169+
override fun decodeShort(): Short = reader.takeString().parse("short") { toShort() }
170+
override fun decodeInt(): Int = reader.takeString().parse("int") { toInt() }
171+
override fun decodeLong(): Long = reader.takeString().parse("long") { toLong() }
172+
override fun decodeFloat(): Float = reader.takeString().parse("float") { toFloat() }
173+
override fun decodeDouble(): Double = reader.takeString().parse("double") { toDouble() }
174+
override fun decodeChar(): Char = reader.takeString().parse("char") { single() }
175+
175176
override fun decodeString(): String {
176177
return if (configuration.isLenient) {
177178
reader.takeString()
@@ -180,6 +181,14 @@ internal class StreamingJsonInput internal constructor(
180181
}
181182
}
182183

184+
private inline fun <T> String.parse(type: String, block: String.() -> T): T {
185+
try {
186+
return block()
187+
} catch (e: Throwable) {
188+
reader.fail("Failed to parse '$type'")
189+
}
190+
}
191+
183192
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
184193
return enumDescriptor.getElementIndexOrThrow(decodeString())
185194
}

runtime/commonMain/src/kotlinx/serialization/json/internal/TreeJsonInput.kt

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,6 @@ private sealed class AbstractJsonTreeInput(
7373

7474
protected abstract fun currentElement(tag: String): JsonElement
7575

76-
override fun decodeTaggedChar(tag: String): Char {
77-
val o = getValue(tag)
78-
return if (o.content.length == 1) o.content[0] else throw SerializationException("$o can't be represented as Char")
79-
}
80-
8176
override fun decodeTaggedEnum(tag: String, enumDescription: SerialDescriptor): Int =
8277
enumDescription.getElementIndexOrThrow(getValue(tag).content)
8378

@@ -100,12 +95,22 @@ private sealed class AbstractJsonTreeInput(
10095
return value.boolean
10196
}
10297

103-
override fun decodeTaggedByte(tag: String): Byte = getValue(tag).int.toByte()
104-
override fun decodeTaggedShort(tag: String) = getValue(tag).int.toShort()
105-
override fun decodeTaggedInt(tag: String) = getValue(tag).int
106-
override fun decodeTaggedLong(tag: String) = getValue(tag).long
107-
override fun decodeTaggedFloat(tag: String) = getValue(tag).float
108-
override fun decodeTaggedDouble(tag: String) = getValue(tag).double
98+
override fun decodeTaggedByte(tag: String) = getValue(tag).primitive("byte") { int.toByte() }
99+
override fun decodeTaggedShort(tag: String) = getValue(tag).primitive("short") { int.toShort() }
100+
override fun decodeTaggedInt(tag: String) = getValue(tag).primitive("int") { int }
101+
override fun decodeTaggedLong(tag: String) = getValue(tag).primitive("long") { long }
102+
override fun decodeTaggedFloat(tag: String) = getValue(tag).primitive("float") { float }
103+
override fun decodeTaggedDouble(tag: String) = getValue(tag).primitive("double") { double }
104+
override fun decodeTaggedChar(tag: String): Char = getValue(tag).primitive("char") { content.single() }
105+
106+
private inline fun <T: Any> JsonPrimitive.primitive(primitive: String, block: JsonPrimitive.() -> T): T {
107+
try {
108+
return block()
109+
} catch (e: Throwable) {
110+
throw JsonDecodingException(-1, "Failed to parse '$primitive'", currentObject().toString())
111+
}
112+
}
113+
109114
override fun decodeTaggedString(tag: String): String {
110115
val value = getValue(tag)
111116
if (!json.configuration.isLenient) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2017-2020 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+
import kotlinx.serialization.*
8+
import kotlin.test.*
9+
10+
class JsonParserFailureModesTest : JsonTestBase() {
11+
12+
@Serializable
13+
data class Holder(
14+
val id: Long
15+
)
16+
17+
@Test
18+
fun testFailureModes() = parametrizedTest {
19+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id": "}""", it) }
20+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id": ""}""", it) }
21+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id":a}""", it) }
22+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id":2.0}""", it) }
23+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id2":2}""", it) }
24+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id"}""", it) }
25+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id}""", it) }
26+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"i}""", it) }
27+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"}""", it) }
28+
assertFailsWith<MissingFieldException> { Json.plain.parse(Holder.serializer(), """{}""", it) }
29+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{""", it) }
30+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """}""", it) }
31+
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{""", it) }
32+
}
33+
}

0 commit comments

Comments
 (0)