@@ -29,6 +29,7 @@ import kotlinx.serialization.MissingFieldException
2929import kotlinx.serialization.SerializationException
3030import kotlinx.serialization.json.Json
3131import kotlinx.serialization.json.JsonPrimitive
32+ import kotlinx.serialization.json.JsonUnquotedLiteral
3233import kotlinx.serialization.json.buildJsonArray
3334import kotlinx.serialization.json.buildJsonObject
3435import kotlinx.serialization.json.put
@@ -47,6 +48,7 @@ import org.bson.BsonInvalidOperationException
4748import org.bson.BsonMaxKey
4849import org.bson.BsonMinKey
4950import org.bson.BsonString
51+ import org.bson.BsonType
5052import org.bson.BsonUndefined
5153import org.bson.codecs.DecoderContext
5254import org.bson.codecs.EncoderContext
@@ -128,6 +130,7 @@ import org.junit.jupiter.api.Test
128130import org.junit.jupiter.api.assertThrows
129131import org.junit.jupiter.params.ParameterizedTest
130132import org.junit.jupiter.params.provider.MethodSource
133+ import org.junit.jupiter.params.provider.ValueSource
131134
132135@OptIn(ExperimentalSerializationApi ::class )
133136@Suppress(" LargeClass" )
@@ -231,21 +234,38 @@ class KotlinSerializerCodecTest {
231234 fun testJsonPrimitiveNumberEncoding (): Stream <Pair <String , String >> {
232235 return Stream .of(
233236 """ {"value": 0}""" to """ {"value": 0}""" ,
234- """ {"value": 0}""" to """ {"value": 0.0}""" ,
237+ """ {"value": 0.0 }""" to """ {"value": 0.0}""" ,
235238 """ {"value": 1.1}""" to """ {"value": 1.1E0}""" ,
236- """ {"value": 11}""" to """ {"value": 1.1E1}""" ,
237- """ {"value": 110}""" to """ {"value": 1.1E2}""" ,
238- """ {"value": 1100}""" to """ {"value": 1.1E3}""" ,
239+ """ {"value": 11.0 }""" to """ {"value": 1.1E1}""" ,
240+ """ {"value": 110.0 }""" to """ {"value": 1.1E2}""" ,
241+ """ {"value": 1100.0 }""" to """ {"value": 1.1E3}""" ,
239242 """ {"value": 0.1}""" to """ {"value": 1E-1}""" ,
240243 """ {"value": 0.01}""" to """ {"value": 1E-2}""" ,
241244 """ {"value": 0.001}""" to """ {"value": 1E-3}""" ,
242245 """ {"value": -1.1}""" to """ {"value": -1.1E0}""" ,
243- """ {"value": -11}""" to """ {"value": -1.1E1}""" ,
244- """ {"value": -110}""" to """ {"value": -1.1E2}""" ,
245- """ {"value": -1100}""" to """ {"value": -1.1E3}""" ,
246+ """ {"value": -11.0 }""" to """ {"value": -1.1E1}""" ,
247+ """ {"value": -110.0 }""" to """ {"value": -1.1E2}""" ,
248+ """ {"value": -1100.0 }""" to """ {"value": -1.1E3}""" ,
246249 """ {"value": -0.1}""" to """ {"value": -1E-1}""" ,
247250 """ {"value": -0.01}""" to """ {"value": -1E-2}""" ,
248251 """ {"value": -0.001}""" to """ {"value": -1E-3}""" ,
252+ """ {"value": -0.0}""" to """ {"value": -0.0}""" ,
253+ """ {"value": 3.0}""" to """ {"value": 3.0}""" ,
254+ """ {"value": 1.0E20}""" to """ {"value": 1.0E20}""" ,
255+ """ {"value": 30.0}""" to """ {"value": 3.0E1}""" ,
256+ // An exponent alone marks a literal as floating point, with or without a fraction,
257+ // matching how org.bson.json.JsonScanner types numbers.
258+ """ {"value": 1.0E20}""" to """ {"value": 1e20}""" ,
259+ """ {"value": -1.0E20}""" to """ {"value": -1e20}""" ,
260+ """ {"value": 100000.0}""" to """ {"value": 1E5}""" ,
261+ """ {"value": 1.0E20}""" to """ {"value": 1e+20}""" ,
262+ // A magnitude beyond Double widens to Decimal128.
263+ // JsonScanner would yield Infinity and 0.0 here.
264+ """ {"value": {"${' $' } numberDecimal": "1E+400"}}""" to """ {"value": 1e400}""" ,
265+ """ {"value": {"${' $' } numberDecimal": "1E-330"}}""" to """ {"value": 1e-330}""" ,
266+ // The negative side of each threshold still encodes as a double.
267+ """ {"value": 1.7976931348623157E308}""" to """ {"value": 1.7976931348623157E308}""" ,
268+ """ {"value": 1.0E-320}""" to """ {"value": 1e-320}""" ,
249269 """ {"value": 9223372036854775807}""" to """ {"value": 9223372036854775807}""" ,
250270 """ {"value": {"${' $' } numberDecimal": "9223372036854775808"}}""" to """ {"value": 9223372036854775808}""" ,
251271 """ {"value": -9223372036854775808}""" to """ {"value": -9223372036854775808}""" ,
@@ -1038,9 +1058,9 @@ class KotlinSerializerCodecTest {
10381058 |"short": 1,
10391059 |"int": 22,
10401060 |"long": {"$numberLong ": "3000000000"},
1041- |"decimal": {"$numberDecimal ": "1E+19 "}
1042- |"decimal2": {"$numberDecimal ": "3.123E +700"}
1043- |"float": 4.1 ,
1061+ |"decimal": {"$numberDecimal ": "10000000000000000000 "}
1062+ |"decimal2": {"$numberDecimal ": "3.1230E +700"}
1063+ |"float": 4.0 ,
10441064 |"double": 4.2,
10451065 |"boolean": true,
10461066 |"string": "String"
@@ -1055,9 +1075,9 @@ class KotlinSerializerCodecTest {
10551075 put(" short" , 1 )
10561076 put(" int" , 22 )
10571077 put(" long" , 3_000_000_000 )
1058- put(" decimal" , BigDecimal (" 1E+19 " ))
1059- put(" decimal2" , BigDecimal (" 3.123E +700" ))
1060- put(" float" , 4.1 )
1078+ put(" decimal" , BigDecimal (" 10000000000000000000 " ))
1079+ put(" decimal2" , BigDecimal (" 3.1230E +700" ))
1080+ put(" float" , 4.0 )
10611081 put(" double" , 4.2 )
10621082 put(" boolean" , true )
10631083 put(" string" , " String" )
@@ -1066,6 +1086,21 @@ class KotlinSerializerCodecTest {
10661086 assertRoundTrips(expected, dataClass)
10671087 }
10681088
1089+ @Test
1090+ fun testDataClassWithJsonElementBigDecimal () {
1091+ // A BigDecimal in a JsonObject is stored as its toString.
1092+ // It is indistinguishable from a hand-written literal.
1093+ // Within double range it encodes as a double, beyond it as a Decimal128.
1094+ // Neither round-trips textually, so this asserts encoding only.
1095+ assertEncodesTo(
1096+ """ {"value": {"withinDouble": 1.0E19, "beyondDouble": {"$numberDecimal ": "1E+400"}}}""" ,
1097+ DataClassWithJsonElement (
1098+ buildJsonObject {
1099+ put(" withinDouble" , BigDecimal (" 1E+19" ))
1100+ put(" beyondDouble" , BigDecimal (" 1E+400" ))
1101+ }))
1102+ }
1103+
10691104 @Test
10701105 fun testDataClassWithJsonElements () {
10711106 val expected =
@@ -1263,6 +1298,62 @@ class KotlinSerializerCodecTest {
12631298 assertEncodesTo(expected, Json .parseToJsonElement(actual))
12641299 }
12651300
1301+ @ParameterizedTest
1302+ @ValueSource(
1303+ strings =
1304+ [
1305+ // Beyond Long and 34 digits: the integral branch.
1306+ " 12345678901234567890123456789012345678901234" ,
1307+ // Beyond Double and 34 digits: the floating branch.
1308+ " 1.2345678901234567890123456789012345678e400" ,
1309+ // Within 34 significant digits, but the exponent exceeds the Decimal128 range.
1310+ " 1E+7000" ,
1311+ " 1E-7000" ])
1312+ fun testJsonPrimitiveNumberExceedingDecimal128 (literal : String ) {
1313+ val exception =
1314+ assertThrows<SerializationException > { serialize(Json .parseToJsonElement(""" {"value": $literal }""" )) }
1315+ assertTrue(exception.message!! .contains(literal), " Should name the literal: ${exception.message} " )
1316+ assertTrue(exception.cause is NumberFormatException , " Should retain the cause: ${exception.cause} " )
1317+ }
1318+
1319+ @ParameterizedTest
1320+ @ValueSource(strings = [" NaN" , " Infinity" , " -Infinity" ])
1321+ fun testJsonPrimitiveNonFiniteDouble (literal : String ) {
1322+ // Double.toString renders these in a form that is not a valid JSON number.
1323+ // Report that rather than leaking a NumberFormatException from BigDecimal.
1324+ val nonFinite = buildJsonObject { put(" value" , JsonPrimitive (literal.toDouble())) }
1325+ val exception = assertThrows<SerializationException > { serialize(nonFinite) }
1326+ assertTrue(exception.message!! .contains(literal), " Should name the literal: ${exception.message} " )
1327+ assertTrue(exception.cause is NumberFormatException , " Should retain the cause: ${exception.cause} " )
1328+ }
1329+
1330+ @ParameterizedTest
1331+ // BigDecimal accepts any Unicode decimal digit, String.toDouble only ASCII.
1332+ // Arabic-Indic and fullwidth digits must not escape as a NumberFormatException.
1333+ @ValueSource(strings = [" ١.٥" , " 1.5" , " ١e٢" , " abc" , " " , " " , " 1_000" , " 0x10" , " 1d" ])
1334+ fun testJsonPrimitiveUnquotedLiteralIsNotANumber (literal : String ) {
1335+ val notANumber = buildJsonObject { put(" value" , JsonUnquotedLiteral (literal)) }
1336+ val exception = assertThrows<SerializationException > { serialize(notANumber) }
1337+ assertTrue(exception.message!! .contains(" not a valid JSON number" ), " Got: ${exception.message} " )
1338+ }
1339+
1340+ @ParameterizedTest
1341+ @MethodSource(" testJsonPrimitiveNumberEncoding" )
1342+ fun testJsonPrimitiveNumberMatchesJsonScanner (test : Pair <String , String >) {
1343+ // isFloatingLiteral keys off the literal text to agree with the driver's own JSON parser.
1344+ // Assert that agreement directly, not only against hand-written expectations.
1345+ // Literals outside the range of the matching BSON type are excluded:
1346+ // there the codec widens to Decimal128 where JsonScanner loses data or throws.
1347+ val literal = test.second
1348+ val viaScanner = runCatching { BsonDocument .parse(literal) }
1349+ val viaCodec = serialize(Json .parseToJsonElement(literal))
1350+ if (viaScanner.getOrNull() == viaCodec) return
1351+ assertEquals(
1352+ BsonType .DECIMAL128 ,
1353+ viaCodec[" value" ]!! .bsonType,
1354+ " Only a widening to Decimal128 may differ from JsonScanner: $literal gave $viaCodec " )
1355+ }
1356+
12661357 @Test
12671358 fun testDataFailures () {
12681359 assertThrows<MissingFieldException >(" Missing data" ) {
0 commit comments