From bcb30a299539cda449417b93e6d7beb624e8c7a3 Mon Sep 17 00:00:00 2001 From: Felix Engelhardt Date: Thu, 9 Apr 2026 15:36:59 +0200 Subject: [PATCH] JsonBsonEncoder: fix parsing of JsonPrimitive numbers encodeJsonPrimitive would in some cases attempt to parse scientifically formatted numbers as plain Ints/Longs, which would result in a NumberFormatException. --- .../bson/codecs/kotlinx/JsonBsonEncoder.kt | 8 ++-- .../kotlinx/KotlinSerializerCodecTest.kt | 47 +++++++++++++++---- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt index 4a754834e6d..3b5fc871829 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt @@ -101,16 +101,16 @@ internal class JsonBsonEncoder( primitive.isString -> encodeString(content) content == "true" || content == "false" -> encodeBoolean(content.toBooleanStrict()) else -> { - val decimal = BigDecimal(content) + val decimal = BigDecimal(content).stripTrailingZeros() when { - decimal.scale() != 0 -> + decimal.scale() > 0 -> if (DOUBLE_MIN_VALUE <= decimal && decimal <= DOUBLE_MAX_VALUE) { encodeDouble(primitive.double) } else { writer.writeDecimal128(Decimal128(decimal)) } - INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE -> encodeInt(primitive.int) - LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE -> encodeLong(primitive.long) + INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE -> encodeInt(decimal.toInt()) + LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE -> encodeLong(decimal.toLong()) else -> writer.writeDecimal128(Decimal128(decimal)) } } diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index f9b3eb753c5..9b92d038f23 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -26,6 +26,7 @@ import kotlinx.datetime.LocalTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject @@ -116,6 +117,7 @@ import org.bson.codecs.kotlinx.samples.SealedInterface import org.bson.codecs.kotlinx.samples.ValueClass import org.bson.json.JsonMode import org.bson.json.JsonWriterSettings +import org.bson.types.Decimal128 import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest @@ -146,10 +148,10 @@ class KotlinSerializerCodecTest { | "code": {"${'$'}code": "int i = 0;"}, | "codeWithScope": {"${'$'}code": "int x = y", "${'$'}scope": {"y": 1}}, | "dateTime": {"${'$'}date": {"${'$'}numberLong": "1577836801000"}}, - | "decimal128": {"${'$'}numberDecimal": "1.0"}, + | "decimal128": {"${'$'}numberDecimal": "1.1"}, | "documentEmpty": {}, | "document": {"a": {"${'$'}numberInt": "1"}}, - | "double": {"${'$'}numberDouble": "62.0"}, + | "double": {"${'$'}numberDouble": "62.1"}, | "int32": {"${'$'}numberInt": "42"}, | "int64": {"${'$'}numberLong": "52"}, | "maxKey": {"${'$'}maxKey": 1}, @@ -218,6 +220,24 @@ class KotlinSerializerCodecTest { .append("boolean", BsonBoolean.TRUE) .append("string", BsonString("String"))) } + + @JvmStatic + fun testJsonPrimitiveNumberEncoding(): Stream> { + return Stream.of( + """{"value": 0}""" to """{"value": 0}""", + """{"value": 0}""" to """{"value": 0.0}""", + """{"value": 1.1}""" to """{"value": 1.1E0}""", + """{"value": 11}""" to """{"value": 1.1E1}""", + """{"value": 110}""" to """{"value": 1.1E2}""", + """{"value": 1100}""" to """{"value": 1.1E3}""", + """{"value": 0.1}""" to """{"value": 1E-1}""", + """{"value": 0.01}""" to """{"value": 1E-2}""", + """{"value": 0.001}""" to """{"value": 1E-3}""", + """{"value": 35485464}""" to """{"value": 35485464}""", + """{"value": 35485464}""" to """{"value": 35485464.0}""", + """{"value": {"${'$'}numberDecimal": "123456789123456789123456789"}}""" to """{"value": 123456789123456789123456789}""" + ) + } } @ParameterizedTest @@ -832,9 +852,9 @@ class KotlinSerializerCodecTest { |"short": 1, |"int": 22, |"long": {"$numberLong": "3000000000"}, - |"decimal": {"$numberDecimal": "10000000000000000000"} - |"decimal2": {"$numberDecimal": "3.1230E+700"} - |"float": 4.0, + |"decimal": {"$numberDecimal": "1E+19"} + |"decimal2": {"$numberDecimal": "3.123E+700"} + |"float": 4.1, |"double": 4.2, |"boolean": true, |"string": "String" @@ -849,9 +869,9 @@ class KotlinSerializerCodecTest { put("short", 1) put("int", 22) put("long", 3_000_000_000) - put("decimal", BigDecimal("10000000000000000000")) - put("decimal2", BigDecimal("3.1230E+700")) - put("float", 4.0) + put("decimal", BigDecimal("1E+19")) + put("decimal2", BigDecimal("3.123E+700")) + put("float", 4.1) put("double", 4.2) put("boolean", true) put("string", "String") @@ -1023,10 +1043,10 @@ class KotlinSerializerCodecTest { put("binary", JsonPrimitive("S2Fma2Egcm9ja3Mh")) put("boolean", JsonPrimitive(true)) put("dateTime", JsonPrimitive(1577836801000)) - put("decimal128", JsonPrimitive(1.0)) + put("decimal128", JsonPrimitive(1.1)) put("documentEmpty", buildJsonObject {}) put("document", buildJsonObject { put("a", JsonPrimitive(1)) }) - put("double", JsonPrimitive(62.0)) + put("double", JsonPrimitive(62.1)) put("int32", JsonPrimitive(42)) put("int64", JsonPrimitive(52)) put("objectId", JsonPrimitive("211111111111111111111112")) @@ -1050,6 +1070,13 @@ class KotlinSerializerCodecTest { assertDecodesTo("""{"value": $jsonAllSupportedTypesDocument}""", dataClassWithAllSupportedJsonTypes) } + @ParameterizedTest + @MethodSource("testJsonPrimitiveNumberEncoding") + fun testJsonPrimitiveNumberEncoding(test: Pair) { + val (expected, actual) = test + assertEncodesTo(expected, Json.parseToJsonElement(actual)) + } + @Test fun testDataFailures() { assertThrows("Missing data") {