Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw SerializationException when encountering an invalid boolean in … #1299

Merged
merged 2 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public val JsonPrimitive.floatOrNull: Float? get() = content.toFloatOrNull()
* Returns content of current element as boolean
* @throws IllegalStateException if current element doesn't represent boolean
*/
public val JsonPrimitive.boolean: Boolean get() = content.toBooleanStrict()
public val JsonPrimitive.boolean: Boolean get() = content.toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean")

/**
* Returns content of current element as boolean or `null` if current element is not a valid representation of boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,13 @@ internal class StreamingJsonDecoder internal constructor(
* We prohibit non true/false boolean literals at all as it is considered way too error-prone,
* but allow quoted literal in relaxed mode for booleans.
*/
return if (configuration.isLenient) {
reader.takeString().toBooleanStrict()
val string = if (configuration.isLenient) {
reader.takeString()
} else {
reader.takeBooleanStringUnquoted().toBooleanStrict()
reader.takeBooleanStringUnquoted()
}
string.toBooleanStrictOrNull()?.let { return it }
reader.fail("Failed to parse type 'boolean' for input '$string'")
}

/*
Expand Down Expand Up @@ -216,8 +218,8 @@ internal class StreamingJsonDecoder internal constructor(
private inline fun <T> String.parse(type: String, block: String.() -> T): T {
try {
return block()
} catch (e: Throwable) {
reader.fail("Failed to parse '$type'")
} catch (e: IllegalArgumentException) {
reader.fail("Failed to parse type '$type' for input '$this'")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,6 @@ internal fun StringBuilder.printQuoted(value: String) {
append(STRING)
}

/**
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false",
* and throws [IllegalStateException] otherwise.
*/
internal fun String.toBooleanStrict(): Boolean = toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean")

/**
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false",
* and returns `null` otherwise.
Expand All @@ -67,12 +61,3 @@ internal fun String.toBooleanStrictOrNull(): Boolean? = when {
this.equals("false", ignoreCase = true) -> false
else -> null
}

internal fun shouldBeQuoted(str: String): Boolean {
if (str == NULL) return true
for (ch in str) {
if (charToTokenClass(ch) != TC_OTHER) return true
}

return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,23 @@ private sealed class AbstractJsonTreeDecoder(
-1, "Boolean literal for key '$tag' should be unquoted.\n$lenientHint", currentObject().toString()
)
}
return value.boolean
return value.primitive("boolean") {
booleanOrNull ?: throw IllegalArgumentException() /* Will be handled by 'primitive' */
}
}

override fun decodeTaggedByte(tag: String) = getValue(tag).primitive("byte") {
val result = int
if (result in Byte.MIN_VALUE..Byte.MAX_VALUE) result.toByte()
else null
}

override fun decodeTaggedShort(tag: String) = getValue(tag).primitive("short") {
val result = int
if (result in Short.MIN_VALUE..Short.MAX_VALUE) result.toShort()
else null
}

override fun decodeTaggedByte(tag: String) = getValue(tag).primitive("byte") { int.toByte() }
override fun decodeTaggedShort(tag: String) = getValue(tag).primitive("short") { int.toShort() }
override fun decodeTaggedInt(tag: String) = getValue(tag).primitive("int") { int }
override fun decodeTaggedLong(tag: String) = getValue(tag).primitive("long") { long }

Expand All @@ -121,14 +133,18 @@ private sealed class AbstractJsonTreeDecoder(

override fun decodeTaggedChar(tag: String): Char = getValue(tag).primitive("char") { content.single() }

private inline fun <T: Any> JsonPrimitive.primitive(primitive: String, block: JsonPrimitive.() -> T): T {
private inline fun <T: Any> JsonPrimitive.primitive(primitive: String, block: JsonPrimitive.() -> T?): T {
try {
return block()
} catch (e: Throwable) {
throw JsonDecodingException(-1, "Failed to parse '$primitive'", currentObject().toString())
return block() ?: unparsedPrimitive(primitive)
} catch (e: IllegalArgumentException) {
unparsedPrimitive(primitive)
}
}

private fun unparsedPrimitive(primitive: String): Nothing {
throw JsonDecodingException(-1, "Failed to parse '$primitive'", currentObject().toString())
}

override fun decodeTaggedString(tag: String): String {
val value = getValue(tag)
if (!json.configuration.isLenient) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ class JsonParserFailureModesTest : JsonTestBase() {
it
)
}
// 9223372036854775807 is Long.MAX_VALUE
assertFailsWith<JsonDecodingException> {
default.decodeFromString(
Holder.serializer(),
"""{"id":${Long.MAX_VALUE}""" + "00" + "}",
it
)
}
assertFailsWith<JsonDecodingException> { default.decodeFromString(Holder.serializer(), """{"id"}""", it) }
assertFailsWith<JsonDecodingException> { default.decodeFromString(Holder.serializer(), """{"id}""", it) }
assertFailsWith<JsonDecodingException> { default.decodeFromString(Holder.serializer(), """{"i}""", it) }
Expand All @@ -64,4 +72,55 @@ class JsonParserFailureModesTest : JsonTestBase() {
assertFailsWith<JsonDecodingException> { default.decodeFromString(Holder.serializer(), """{""", it) }
}
}

@Serializable
class BooleanHolder(val b: Boolean)

@Test
fun testBoolean() = parametrizedTest {
assertFailsWith<JsonDecodingException> {
default.decodeFromString(
BooleanHolder.serializer(),
"""{"b": fals}""",
it
)
}
assertFailsWith<JsonDecodingException> {
default.decodeFromString(
BooleanHolder.serializer(),
"""{"b": 123}""",
it
)
}
}

@Serializable
class PrimitiveHolder(
val b: Byte = 0, val s: Short = 0, val i: Int = 0
)

@Test
fun testOverflow() = parametrizedTest {
// Byte overflow
assertFailsWith<JsonDecodingException> { default.decodeFromString<PrimitiveHolder>("""{"b": 128}""", it) }
// Short overflow
assertFailsWith<JsonDecodingException> { default.decodeFromString<PrimitiveHolder>("""{"s": 32768}""", it) }
// Int overflow
assertFailsWith<JsonDecodingException> {
default.decodeFromString<PrimitiveHolder>(
"""{"i": 2147483648}""",
it
)
}
}

@Test
fun testNoOverflow() = parametrizedTest {
default.decodeFromString<PrimitiveHolder>("""{"b": ${Byte.MAX_VALUE}}""", it)
default.decodeFromString<PrimitiveHolder>("""{"b": ${Byte.MIN_VALUE}}""", it)
default.decodeFromString<PrimitiveHolder>("""{"s": ${Short.MAX_VALUE}}""", it)
default.decodeFromString<PrimitiveHolder>("""{"s": ${Short.MIN_VALUE}}""", it)
default.decodeFromString<PrimitiveHolder>("""{"i": ${Int.MAX_VALUE}}""", it)
default.decodeFromString<PrimitiveHolder>("""{"i": ${Int.MIN_VALUE}}""", it)
}
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,45 @@
package kotlinx.serialization.json

import com.google.gson.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import org.junit.Test
import kotlin.test.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import org.junit.*

class GsonCompatibilityTest {

@Serializable
data class Box(val d: Double, val f: Float)
@Serializable(with = ValueSerializer::class)
data class Value<T>(val isSet: Boolean, val value: T?)

@Test
fun testNaN() {
checkCompatibility(Box(Double.NaN, 1.0f))
checkCompatibility(Box(1.0, Float.NaN))
checkCompatibility(Box(Double.NaN, Float.NaN))
}
class ValueSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Value<T>> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Value", PrimitiveKind.STRING).nullable

@Test
fun testInfinity() {
checkCompatibility(Box(Double.POSITIVE_INFINITY, 1.0f))
checkCompatibility(Box(1.0, Float.POSITIVE_INFINITY))
checkCompatibility(Box(Double.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY))
override fun serialize(encoder: Encoder, value: Value<T>) {
encoder.encodeNullableSerializableValue(dataSerializer, value.value)
}

@Test
fun testNumber() {
checkCompatibility(Box(23.9, 23.9f))
}
override fun deserialize(decoder: Decoder) = TODO("Not implemented!")
}

private fun checkCompatibility(box: Box) {
checkCompatibility(box, Gson(), Json)
checkCompatibility(box, GsonBuilder().serializeSpecialFloatingPointValues().create(), Json { allowSpecialFloatingPointValues = true })
class ValueClassSerializer<T : Any>(private val dataSerializer: KSerializer<T>) :
JsonTransformingSerializer<T>(dataSerializer) {
override fun transformSerialize(element: JsonElement): JsonElement =
element.jsonObject
}

private fun checkCompatibility(box: Box, gson: Gson, json: Json) {
val jsonResult = resultOrNull { json.encodeToString(box) }
val gsonResult = resultOrNull { gson.toJson(box) }
assertEquals(gsonResult, jsonResult)

if (jsonResult != null && gsonResult != null) {
val jsonDeserialized: Box = json.decodeFromString(jsonResult)
val gsonDeserialized: Box = gson.fromJson(gsonResult, Box::class.java)
assertEquals(gsonDeserialized, jsonDeserialized)
}
}
@Serializable
data class TestObject(
val test1: Value<String> = Value(true, "Hello World"),
val test2: Value<String> = Value(false, null),
val test3: Value<String> = Value(true, null),
)

private fun resultOrNull(function: () -> String): String? {
return try {
function()
} catch (t: Throwable) {
null
}
@Test
fun f() {
println(
Json { encodeDefaults = true }.encodeToString(
ValueClassSerializer(TestObject.serializer()),
TestObject()
)
)
}
}