Skip to content

Commit 88493e9

Browse files
committed
Merge branch 'main' into JAVA-6057
# Conflicts: # driver-core/src/main/com/mongodb/internal/observability/micrometer/TracingManager.java # driver-sync/src/test/functional/com/mongodb/client/AbstractMicrometerProseTest.java
2 parents 4893ffa + 99108af commit 88493e9

41 files changed

Lines changed: 1392 additions & 331 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ internal data class DataClassCodec<T : Any>(
7676
@Suppress("TooGenericExceptionCaught")
7777
override fun decode(reader: BsonReader, decoderContext: DecoderContext): T {
7878
val args: MutableMap<KParameter, Any?> = mutableMapOf()
79-
fieldNamePropertyModelMap.values.forEach { args[it.param] = null }
8079

8180
reader.readStartDocument()
8281
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
@@ -89,6 +88,7 @@ internal data class DataClassCodec<T : Any>(
8988
}
9089
} else if (propertyModel.param.type.isMarkedNullable && reader.currentBsonType == BsonType.NULL) {
9190
reader.readNull()
91+
args[propertyModel.param] = null
9292
} else {
9393
try {
9494
args[propertyModel.param] = decoderContext.decodeWithChildContext(propertyModel.codec, reader)
@@ -100,6 +100,23 @@ internal data class DataClassCodec<T : Any>(
100100
}
101101
reader.readEndDocument()
102102

103+
// For non-optional parameters missing from the document, fail with a clear message
104+
// if non-nullable, or pass null explicitly if nullable.
105+
// Optional parameters (with defaults) are left absent so callBy uses the default value.
106+
fieldNamePropertyModelMap.values.forEach {
107+
if (it.param !in args && !it.param.isOptional) {
108+
// Only error for concrete types (KClass). Generic type parameters (KTypeParameter)
109+
// may be nullable at runtime even though isMarkedNullable is false at the
110+
// declaration site (e.g. Box<T>(val boxed: T) instantiated as Box<String?>).
111+
if (!it.param.type.isMarkedNullable && it.param.type.classifier is KClass<*>) {
112+
throw CodecConfigurationException(
113+
"Required field '${it.fieldName}' is missing from the document for " +
114+
"${kClass.simpleName} data class")
115+
}
116+
args[it.param] = null
117+
}
118+
}
119+
103120
try {
104121
return primaryConstructor.callBy(args)
105122
} catch (e: Exception) {

bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import org.bson.codecs.kotlin.samples.DataClassWithBsonProperty
4848
import org.bson.codecs.kotlin.samples.DataClassWithCollections
4949
import org.bson.codecs.kotlin.samples.DataClassWithDataClassMapKey
5050
import org.bson.codecs.kotlin.samples.DataClassWithDefaults
51+
import org.bson.codecs.kotlin.samples.DataClassWithDefaultsAndNulls
5152
import org.bson.codecs.kotlin.samples.DataClassWithEmbedded
5253
import org.bson.codecs.kotlin.samples.DataClassWithEnum
5354
import org.bson.codecs.kotlin.samples.DataClassWithEnumMapKey
@@ -177,17 +178,71 @@ class DataClassCodecTest {
177178
|}"""
178179
.trimMargin()
179180

180-
val defaultDataClass = DataClassWithDefaults()
181-
assertRoundTrips(expectedDefault, defaultDataClass)
181+
assertRoundTrips(expectedDefault, DataClassWithDefaults())
182+
183+
// Assert no data decodes as expected
184+
assertDecodesTo(BsonDocument.parse(emptyDocument), DataClassWithDefaults())
185+
186+
// Assert some data
187+
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithDefaults(string = "Custom"))
188+
189+
// Assert all data
190+
val expected =
191+
"""{
192+
| "boolean": true,
193+
| "string": "Custom",
194+
| "listSimple": ["x"]
195+
|}"""
196+
.trimMargin()
197+
198+
assertRoundTrips(expected, DataClassWithDefaults(boolean = true, string = "Custom", listSimple = listOf("x")))
182199
}
183200

184201
@Test
185202
fun testDataClassWithNulls() {
186203
val dataClass = DataClassWithNulls(null, null, null)
187204
assertRoundTrips(emptyDocument, dataClass)
188205

189-
val withStoredNulls = BsonDocument.parse("""{"boolean": null, "string": null, "listSimple": null}""")
190-
assertDecodesTo(withStoredNulls, dataClass)
206+
// Assert all null data decodes as expected
207+
assertDecodesTo(BsonDocument.parse("""{"boolean": null, "string": null, "listSimple": null}"""), dataClass)
208+
209+
// Assert some data
210+
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithNulls(null, "Custom", null))
211+
212+
// Assert all data
213+
val expected =
214+
"""{
215+
| "boolean": true,
216+
| "string": "Custom",
217+
| "listSimple": ["x"]
218+
|}"""
219+
.trimMargin()
220+
assertRoundTrips(expected, DataClassWithNulls(true, "Custom", listOf("x")))
221+
}
222+
223+
@Test
224+
fun testDataClassWithDefaultsAndNulls() {
225+
// All fields provided
226+
val expected = """{"required": "req", "optional": "opt", "nullable": "nul"}"""
227+
assertRoundTrips(expected, DataClassWithDefaultsAndNulls("req", "opt", "nul"))
228+
229+
// Only required field — optional gets default, nullable gets default (null)
230+
assertDecodesTo(BsonDocument.parse("""{"required": "req"}"""), DataClassWithDefaultsAndNulls("req"))
231+
232+
// Required + nullable explicit null in document
233+
assertDecodesTo(
234+
BsonDocument.parse("""{"required": "req", "nullable": null}"""), DataClassWithDefaultsAndNulls("req"))
235+
236+
// Required + optional overridden, nullable absent
237+
assertDecodesTo(
238+
BsonDocument.parse("""{"required": "req", "optional": "custom"}"""),
239+
DataClassWithDefaultsAndNulls("req", "custom"))
240+
241+
// Missing required field throws
242+
assertThrows<CodecConfigurationException> {
243+
val codec = DataClassCodec.create(DataClassWithDefaultsAndNulls::class, registry())
244+
codec?.decode(BsonDocumentReader(BsonDocument()), DecoderContext.builder().build())
245+
}
191246
}
192247

193248
@Test

bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ data class DataClassWithDefaults(
142142

143143
data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List<String?>?)
144144

145+
data class DataClassWithDefaultsAndNulls(
146+
val required: String,
147+
val optional: String = "default",
148+
val nullable: String? = null
149+
)
150+
145151
data class DataClassWithListThatLastItemDefaultsToNull(val elements: List<DataClassLastItemDefaultsToNull>)
146152

147153
data class DataClassLastItemDefaultsToNull(val required: String, val optional: String? = null)

bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ import kotlinx.serialization.json.JsonEncoder
2525
import kotlinx.serialization.json.JsonNull
2626
import kotlinx.serialization.json.JsonObject
2727
import kotlinx.serialization.json.JsonPrimitive
28-
import kotlinx.serialization.json.double
29-
import kotlinx.serialization.json.int
30-
import kotlinx.serialization.json.long
3128
import kotlinx.serialization.modules.SerializersModule
3229
import org.bson.BsonWriter
3330
import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy
@@ -41,8 +38,8 @@ internal class JsonBsonEncoder(
4138
) : BsonEncoderImpl(writer, serializersModule, configuration), JsonEncoder {
4239

4340
companion object {
44-
private val DOUBLE_MIN_VALUE = BigDecimal.valueOf(Double.MIN_VALUE)
4541
private val DOUBLE_MAX_VALUE = BigDecimal.valueOf(Double.MAX_VALUE)
42+
private val DOUBLE_MIN_VALUE = BigDecimal.valueOf(Double.MIN_VALUE)
4643
private val INT_MIN_VALUE = BigDecimal.valueOf(Int.MIN_VALUE.toLong())
4744
private val INT_MAX_VALUE = BigDecimal.valueOf(Int.MAX_VALUE.toLong())
4845
private val LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE)
@@ -101,16 +98,18 @@ internal class JsonBsonEncoder(
10198
primitive.isString -> encodeString(content)
10299
content == "true" || content == "false" -> encodeBoolean(content.toBooleanStrict())
103100
else -> {
104-
val decimal = BigDecimal(content)
101+
val decimal = BigDecimal(content).stripTrailingZeros()
105102
when {
106-
decimal.scale() != 0 ->
107-
if (DOUBLE_MIN_VALUE <= decimal && decimal <= DOUBLE_MAX_VALUE) {
108-
encodeDouble(primitive.double)
103+
decimal.scale() > 0 -> {
104+
val abs = decimal.abs()
105+
if ((decimal.signum() == 0 || abs >= DOUBLE_MIN_VALUE) && abs <= DOUBLE_MAX_VALUE) {
106+
encodeDouble(decimal.toDouble())
109107
} else {
110108
writer.writeDecimal128(Decimal128(decimal))
111109
}
112-
INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE -> encodeInt(primitive.int)
113-
LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE -> encodeLong(primitive.long)
110+
}
111+
INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE -> encodeInt(decimal.toInt())
112+
LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE -> encodeLong(decimal.toLong())
114113
else -> writer.writeDecimal128(Decimal128(decimal))
115114
}
116115
}

bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import kotlinx.datetime.LocalTime
2626
import kotlinx.serialization.ExperimentalSerializationApi
2727
import kotlinx.serialization.MissingFieldException
2828
import kotlinx.serialization.SerializationException
29+
import kotlinx.serialization.json.Json
2930
import kotlinx.serialization.json.JsonPrimitive
3031
import kotlinx.serialization.json.buildJsonArray
3132
import kotlinx.serialization.json.buildJsonObject
@@ -87,6 +88,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithContextualDateValues
8788
import org.bson.codecs.kotlinx.samples.DataClassWithDataClassMapKey
8889
import org.bson.codecs.kotlinx.samples.DataClassWithDateValues
8990
import org.bson.codecs.kotlinx.samples.DataClassWithDefaults
91+
import org.bson.codecs.kotlinx.samples.DataClassWithDefaultsAndNulls
9092
import org.bson.codecs.kotlinx.samples.DataClassWithEmbedded
9193
import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault
9294
import org.bson.codecs.kotlinx.samples.DataClassWithEnum
@@ -146,10 +148,10 @@ class KotlinSerializerCodecTest {
146148
| "code": {"${'$'}code": "int i = 0;"},
147149
| "codeWithScope": {"${'$'}code": "int x = y", "${'$'}scope": {"y": 1}},
148150
| "dateTime": {"${'$'}date": {"${'$'}numberLong": "1577836801000"}},
149-
| "decimal128": {"${'$'}numberDecimal": "1.0"},
151+
| "decimal128": {"${'$'}numberDecimal": "1.1"},
150152
| "documentEmpty": {},
151153
| "document": {"a": {"${'$'}numberInt": "1"}},
152-
| "double": {"${'$'}numberDouble": "62.0"},
154+
| "double": {"${'$'}numberDouble": "62.1"},
153155
| "int32": {"${'$'}numberInt": "42"},
154156
| "int64": {"${'$'}numberLong": "52"},
155157
| "maxKey": {"${'$'}maxKey": 1},
@@ -218,6 +220,35 @@ class KotlinSerializerCodecTest {
218220
.append("boolean", BsonBoolean.TRUE)
219221
.append("string", BsonString("String")))
220222
}
223+
224+
@JvmStatic
225+
fun testJsonPrimitiveNumberEncoding(): Stream<Pair<String, String>> {
226+
return Stream.of(
227+
"""{"value": 0}""" to """{"value": 0}""",
228+
"""{"value": 0}""" to """{"value": 0.0}""",
229+
"""{"value": 1.1}""" to """{"value": 1.1E0}""",
230+
"""{"value": 11}""" to """{"value": 1.1E1}""",
231+
"""{"value": 110}""" to """{"value": 1.1E2}""",
232+
"""{"value": 1100}""" to """{"value": 1.1E3}""",
233+
"""{"value": 0.1}""" to """{"value": 1E-1}""",
234+
"""{"value": 0.01}""" to """{"value": 1E-2}""",
235+
"""{"value": 0.001}""" to """{"value": 1E-3}""",
236+
"""{"value": -1.1}""" to """{"value": -1.1E0}""",
237+
"""{"value": -11}""" to """{"value": -1.1E1}""",
238+
"""{"value": -110}""" to """{"value": -1.1E2}""",
239+
"""{"value": -1100}""" to """{"value": -1.1E3}""",
240+
"""{"value": -0.1}""" to """{"value": -1E-1}""",
241+
"""{"value": -0.01}""" to """{"value": -1E-2}""",
242+
"""{"value": -0.001}""" to """{"value": -1E-3}""",
243+
"""{"value": 9223372036854775807}""" to """{"value": 9223372036854775807}""",
244+
"""{"value": {"${'$'}numberDecimal": "9223372036854775808"}}""" to """{"value": 9223372036854775808}""",
245+
"""{"value": -9223372036854775808}""" to """{"value": -9223372036854775808}""",
246+
"""{"value": {"${'$'}numberDecimal": "-9223372036854775809"}}""" to
247+
"""{"value": -9223372036854775809}""",
248+
"""{"value": {"${'$'}numberDecimal": "1.8E+309"}}""" to """{"value": 1.8E+309}""",
249+
"""{"value": {"${'$'}numberDecimal": "1E-325"}}""" to """{"value": 1E-325}""",
250+
)
251+
}
221252
}
222253

223254
@ParameterizedTest
@@ -303,28 +334,71 @@ class KotlinSerializerCodecTest {
303334
|}"""
304335
.trimMargin()
305336

306-
val defaultDataClass = DataClassWithDefaults()
307-
assertRoundTrips(expectedDefault, defaultDataClass)
308-
assertRoundTrips(emptyDocument, defaultDataClass, altConfiguration)
337+
assertRoundTrips(expectedDefault, DataClassWithDefaults())
309338

310-
val expectedSomeOverrides = """{"boolean": true, "listSimple": ["a"]}"""
311-
val someOverridesDataClass = DataClassWithDefaults(boolean = true, listSimple = listOf("a"))
312-
assertRoundTrips(expectedSomeOverrides, someOverridesDataClass, altConfiguration)
339+
// Assert no data decodes as expected
340+
assertDecodesTo(BsonDocument.parse(emptyDocument), DataClassWithDefaults())
341+
342+
// Assert some data
343+
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithDefaults(string = "Custom"))
344+
345+
// Assert all data
346+
val expected =
347+
"""{
348+
| "boolean": true,
349+
| "string": "Custom",
350+
| "listSimple": ["x"]
351+
|}"""
352+
.trimMargin()
353+
354+
assertRoundTrips(expected, DataClassWithDefaults(boolean = true, string = "Custom", listSimple = listOf("x")))
313355
}
314356

315357
@Test
316358
fun testDataClassWithNulls() {
317-
val expectedNulls =
359+
val dataClass = DataClassWithNulls(null, null, null)
360+
assertRoundTrips(emptyDocument, dataClass)
361+
362+
// Assert all null data decodes as expected
363+
assertDecodesTo(BsonDocument.parse("""{"boolean": null, "string": null, "listSimple": null}"""), dataClass)
364+
365+
// Assert some data
366+
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithNulls(null, "Custom", null))
367+
368+
// Assert all data
369+
val expected =
318370
"""{
319-
| "boolean": null,
320-
| "string": null,
321-
| "listSimple": null
371+
| "boolean": true,
372+
| "string": "Custom",
373+
| "listSimple": ["x"]
322374
|}"""
323375
.trimMargin()
376+
assertRoundTrips(expected, DataClassWithNulls(true, "Custom", listOf("x")))
377+
}
324378

325-
val dataClass = DataClassWithNulls(null, null, null)
326-
assertRoundTrips(emptyDocument, dataClass)
327-
assertRoundTrips(expectedNulls, dataClass, altConfiguration)
379+
@Test
380+
fun testDataClassWithDefaultsAndNulls() {
381+
// All fields provided
382+
val expected = """{"required": "req", "optional": "opt", "nullable": "nul"}"""
383+
assertRoundTrips(expected, DataClassWithDefaultsAndNulls("req", "opt", "nul"))
384+
385+
// Only required field — optional gets default, nullable gets default (null)
386+
assertDecodesTo(BsonDocument.parse("""{"required": "req"}"""), DataClassWithDefaultsAndNulls("req"))
387+
388+
// Required + nullable explicit null in document
389+
assertDecodesTo(
390+
BsonDocument.parse("""{"required": "req", "nullable": null}"""), DataClassWithDefaultsAndNulls("req"))
391+
392+
// Required + optional overridden, nullable absent
393+
assertDecodesTo(
394+
BsonDocument.parse("""{"required": "req", "optional": "custom"}"""),
395+
DataClassWithDefaultsAndNulls("req", "custom"))
396+
397+
// Missing required field throws
398+
assertThrows<MissingFieldException> {
399+
val codec = KotlinSerializerCodec.create(DataClassWithDefaultsAndNulls::class)
400+
codec?.decode(BsonDocumentReader(BsonDocument()), DecoderContext.builder().build())
401+
}
328402
}
329403

330404
@Test
@@ -832,9 +906,9 @@ class KotlinSerializerCodecTest {
832906
|"short": 1,
833907
|"int": 22,
834908
|"long": {"$numberLong": "3000000000"},
835-
|"decimal": {"$numberDecimal": "10000000000000000000"}
836-
|"decimal2": {"$numberDecimal": "3.1230E+700"}
837-
|"float": 4.0,
909+
|"decimal": {"$numberDecimal": "1E+19"}
910+
|"decimal2": {"$numberDecimal": "3.123E+700"}
911+
|"float": 4.1,
838912
|"double": 4.2,
839913
|"boolean": true,
840914
|"string": "String"
@@ -849,9 +923,9 @@ class KotlinSerializerCodecTest {
849923
put("short", 1)
850924
put("int", 22)
851925
put("long", 3_000_000_000)
852-
put("decimal", BigDecimal("10000000000000000000"))
853-
put("decimal2", BigDecimal("3.1230E+700"))
854-
put("float", 4.0)
926+
put("decimal", BigDecimal("1E+19"))
927+
put("decimal2", BigDecimal("3.123E+700"))
928+
put("float", 4.1)
855929
put("double", 4.2)
856930
put("boolean", true)
857931
put("string", "String")
@@ -1023,10 +1097,10 @@ class KotlinSerializerCodecTest {
10231097
put("binary", JsonPrimitive("S2Fma2Egcm9ja3Mh"))
10241098
put("boolean", JsonPrimitive(true))
10251099
put("dateTime", JsonPrimitive(1577836801000))
1026-
put("decimal128", JsonPrimitive(1.0))
1100+
put("decimal128", JsonPrimitive(1.1))
10271101
put("documentEmpty", buildJsonObject {})
10281102
put("document", buildJsonObject { put("a", JsonPrimitive(1)) })
1029-
put("double", JsonPrimitive(62.0))
1103+
put("double", JsonPrimitive(62.1))
10301104
put("int32", JsonPrimitive(42))
10311105
put("int64", JsonPrimitive(52))
10321106
put("objectId", JsonPrimitive("211111111111111111111112"))
@@ -1050,6 +1124,13 @@ class KotlinSerializerCodecTest {
10501124
assertDecodesTo("""{"value": $jsonAllSupportedTypesDocument}""", dataClassWithAllSupportedJsonTypes)
10511125
}
10521126

1127+
@ParameterizedTest
1128+
@MethodSource("testJsonPrimitiveNumberEncoding")
1129+
fun testJsonPrimitiveNumberEncoding(test: Pair<String, String>) {
1130+
val (expected, actual) = test
1131+
assertEncodesTo(expected, Json.parseToJsonElement(actual))
1132+
}
1133+
10531134
@Test
10541135
fun testDataFailures() {
10551136
assertThrows<MissingFieldException>("Missing data") {

bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ data class DataClassWithKotlinAllowedName(
129129

130130
@Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List<String?>?)
131131

132+
@Serializable
133+
data class DataClassWithDefaultsAndNulls(
134+
val required: String,
135+
val optional: String = "default",
136+
val nullable: String? = null
137+
)
138+
132139
@Serializable
133140
data class DataClassWithListThatLastItemDefaultsToNull(val elements: List<DataClassLastItemDefaultsToNull>)
134141

0 commit comments

Comments
 (0)