Skip to content

Commit c5370c3

Browse files
authored
Kotlinx serialization decoding optional ObjectId / BsonValues fails to hydrate properly
Fixes element decoding logic and ensuring all element indexes are accounted for. JAVA-5031
1 parent 0f68e1e commit c5370c3

File tree

5 files changed

+242
-68
lines changed

5 files changed

+242
-68
lines changed

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

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import kotlinx.serialization.descriptors.PrimitiveKind
2323
import kotlinx.serialization.descriptors.SerialDescriptor
2424
import kotlinx.serialization.descriptors.SerialKind
2525
import kotlinx.serialization.descriptors.StructureKind
26-
import kotlinx.serialization.descriptors.elementDescriptors
2726
import kotlinx.serialization.encoding.AbstractDecoder
2827
import kotlinx.serialization.encoding.CompositeDecoder
2928
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
@@ -61,55 +60,56 @@ internal open class DefaultBsonDecoder(
6160
internal val configuration: BsonConfiguration
6261
) : BsonDecoder, AbstractDecoder() {
6362

63+
private data class ElementMetadata(val name: String, val nullable: Boolean, var processed: Boolean = false)
64+
private var elementsMetadata: Array<ElementMetadata>? = null
65+
private var currentIndex: Int = UNKNOWN_INDEX
66+
6467
companion object {
6568
val validKeyKinds = setOf(PrimitiveKind.STRING, PrimitiveKind.CHAR, SerialKind.ENUM)
6669
val bsonValueCodec = BsonValueCodec()
70+
const val UNKNOWN_INDEX = -10
6771
}
6872

69-
private var elementsIsNullableIndexes: BooleanArray? = null
70-
71-
private fun initElementNullsIndexes(descriptor: SerialDescriptor) {
72-
if (elementsIsNullableIndexes != null) return
73-
val elementIndexes = BooleanArray(descriptor.elementsCount)
74-
descriptor.elementDescriptors.withIndex().forEach {
75-
elementIndexes[it.index] = !descriptor.isElementOptional(it.index) && it.value.isNullable
76-
}
77-
elementsIsNullableIndexes = elementIndexes
73+
private fun initElementMetadata(descriptor: SerialDescriptor) {
74+
if (this.elementsMetadata != null) return
75+
val elementsMetadata =
76+
Array(descriptor.elementsCount) {
77+
val elementDescriptor = descriptor.getElementDescriptor(it)
78+
ElementMetadata(
79+
elementDescriptor.serialName, elementDescriptor.isNullable && !descriptor.isElementOptional(it))
80+
}
81+
this.elementsMetadata = elementsMetadata
7882
}
7983

80-
@Suppress("ReturnCount")
8184
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
82-
initElementNullsIndexes(descriptor)
85+
initElementMetadata(descriptor)
86+
currentIndex = decodeElementIndexImpl(descriptor)
87+
elementsMetadata?.getOrNull(currentIndex)?.processed = true
88+
return currentIndex
89+
}
8390

91+
@Suppress("ReturnCount", "ComplexMethod")
92+
private fun decodeElementIndexImpl(descriptor: SerialDescriptor): Int {
93+
val elementMetadata = elementsMetadata ?: error("elementsMetadata may not be null.")
8494
val name: String? =
8595
when (reader.state ?: error("State of reader may not be null.")) {
8696
AbstractBsonReader.State.NAME -> reader.readName()
8797
AbstractBsonReader.State.VALUE -> reader.currentName
8898
AbstractBsonReader.State.TYPE -> {
8999
reader.readBsonType()
90-
return decodeElementIndex(descriptor)
100+
return decodeElementIndexImpl(descriptor)
91101
}
92102
AbstractBsonReader.State.END_OF_DOCUMENT,
93-
AbstractBsonReader.State.END_OF_ARRAY -> {
94-
val isNullableIndexes =
95-
elementsIsNullableIndexes ?: error("elementsIsNullableIndexes may not be null.")
96-
val indexOfNullableElement = isNullableIndexes.indexOfFirst { it }
97-
98-
return if (indexOfNullableElement == -1) {
99-
DECODE_DONE
100-
} else {
101-
isNullableIndexes[indexOfNullableElement] = false
102-
indexOfNullableElement
103-
}
104-
}
103+
AbstractBsonReader.State.END_OF_ARRAY ->
104+
return elementMetadata.indexOfFirst { it.nullable && !it.processed }
105105
else -> null
106106
}
107107

108108
return name?.let {
109109
val index = descriptor.getElementIndex(it)
110110
return if (index == UNKNOWN_NAME) {
111111
reader.skipValue()
112-
decodeElementIndex(descriptor)
112+
decodeElementIndexImpl(descriptor)
113113
} else {
114114
index
115115
}

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

Lines changed: 140 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ import org.bson.BsonDocument
2727
import org.bson.BsonDocumentReader
2828
import org.bson.BsonDocumentWriter
2929
import org.bson.BsonInvalidOperationException
30+
import org.bson.BsonMaxKey
31+
import org.bson.BsonMinKey
32+
import org.bson.BsonUndefined
3033
import org.bson.codecs.DecoderContext
3134
import org.bson.codecs.EncoderContext
3235
import org.bson.codecs.configuration.CodecConfigurationException
36+
import org.bson.codecs.kotlinx.samples.DataClassBsonValues
3337
import org.bson.codecs.kotlinx.samples.DataClassContainsOpen
3438
import org.bson.codecs.kotlinx.samples.DataClassContainsValueClass
3539
import org.bson.codecs.kotlinx.samples.DataClassEmbedded
@@ -43,6 +47,7 @@ import org.bson.codecs.kotlinx.samples.DataClassNestedParameterizedTypes
4347
import org.bson.codecs.kotlinx.samples.DataClassOpen
4448
import org.bson.codecs.kotlinx.samples.DataClassOpenA
4549
import org.bson.codecs.kotlinx.samples.DataClassOpenB
50+
import org.bson.codecs.kotlinx.samples.DataClassOptionalBsonValues
4651
import org.bson.codecs.kotlinx.samples.DataClassParameterized
4752
import org.bson.codecs.kotlinx.samples.DataClassSealed
4853
import org.bson.codecs.kotlinx.samples.DataClassSealedA
@@ -72,7 +77,6 @@ import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet
7277
import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterized
7378
import org.bson.codecs.kotlinx.samples.DataClassWithNestedParameterizedDataClass
7479
import org.bson.codecs.kotlinx.samples.DataClassWithNulls
75-
import org.bson.codecs.kotlinx.samples.DataClassWithObjectIdAndBsonDocument
7680
import org.bson.codecs.kotlinx.samples.DataClassWithPair
7781
import org.bson.codecs.kotlinx.samples.DataClassWithParameterizedDataClass
7882
import org.bson.codecs.kotlinx.samples.DataClassWithRequired
@@ -81,7 +85,6 @@ import org.bson.codecs.kotlinx.samples.DataClassWithSimpleValues
8185
import org.bson.codecs.kotlinx.samples.DataClassWithTriple
8286
import org.bson.codecs.kotlinx.samples.Key
8387
import org.bson.codecs.kotlinx.samples.ValueClass
84-
import org.bson.types.ObjectId
8588
import org.junit.jupiter.api.Test
8689
import org.junit.jupiter.api.assertThrows
8790

@@ -92,6 +95,40 @@ class KotlinSerializerCodecTest {
9295
private val altConfiguration =
9396
BsonConfiguration(encodeDefaults = false, classDiscriminator = "_t", explicitNulls = true)
9497

98+
private val allBsonTypesJson =
99+
"""{
100+
| "id": {"${'$'}oid": "111111111111111111111111"},
101+
| "arrayEmpty": [],
102+
| "arraySimple": [{"${'$'}numberInt": "1"}, {"${'$'}numberInt": "2"}, {"${'$'}numberInt": "3"}],
103+
| "arrayComplex": [{"a": {"${'$'}numberInt": "1"}}, {"a": {"${'$'}numberInt": "2"}}],
104+
| "arrayMixedTypes": [{"${'$'}numberInt": "1"}, {"${'$'}numberInt": "2"}, true,
105+
| [{"${'$'}numberInt": "1"}, {"${'$'}numberInt": "2"}, {"${'$'}numberInt": "3"}],
106+
| {"a": {"${'$'}numberInt": "2"}}],
107+
| "arrayComplexMixedTypes": [{"a": {"${'$'}numberInt": "1"}}, {"a": "a"}],
108+
| "binary": {"${'$'}binary": {"base64": "S2Fma2Egcm9ja3Mh", "subType": "00"}},
109+
| "boolean": true,
110+
| "code": {"${'$'}code": "int i = 0;"},
111+
| "codeWithScope": {"${'$'}code": "int x = y", "${'$'}scope": {"y": {"${'$'}numberInt": "1"}}},
112+
| "dateTime": {"${'$'}date": {"${'$'}numberLong": "1577836801000"}},
113+
| "decimal128": {"${'$'}numberDecimal": "1.0"},
114+
| "documentEmpty": {},
115+
| "document": {"a": {"${'$'}numberInt": "1"}},
116+
| "double": {"${'$'}numberDouble": "62.0"},
117+
| "int32": {"${'$'}numberInt": "42"},
118+
| "int64": {"${'$'}numberLong": "52"},
119+
| "maxKey": {"${'$'}maxKey": 1},
120+
| "minKey": {"${'$'}minKey": 1},
121+
| "objectId": {"${'$'}oid": "211111111111111111111112"},
122+
| "regex": {"${'$'}regularExpression": {"pattern": "^test.*regex.*xyz$", "options": "i"}},
123+
| "string": "the fox ...",
124+
| "symbol": {"${'$'}symbol": "ruby stuff"},
125+
| "timestamp": {"${'$'}timestamp": {"t": 305419896, "i": 5}},
126+
| "undefined": {"${'$'}undefined": true}
127+
| }"""
128+
.trimMargin()
129+
130+
private val allBsonTypesDocument = BsonDocument.parse(allBsonTypesJson)
131+
95132
@Test
96133
fun testDataClassWithSimpleValues() {
97134
val expected =
@@ -432,44 +469,109 @@ class KotlinSerializerCodecTest {
432469
}
433470

434471
@Test
435-
fun testDataClassWithObjectIdAndBsonDocument() {
436-
val subDocument =
437-
"""{
438-
| "_id": 1,
439-
| "arrayEmpty": [],
440-
| "arraySimple": [{"${'$'}numberInt": "1"}, {"${'$'}numberInt": "2"}, {"${'$'}numberInt": "3"}],
441-
| "arrayComplex": [{"a": {"${'$'}numberInt": "1"}}, {"a": {"${'$'}numberInt": "2"}}],
442-
| "arrayMixedTypes": [{"${'$'}numberInt": "1"}, {"${'$'}numberInt": "2"}, true,
443-
| [{"${'$'}numberInt": "1"}, {"${'$'}numberInt": "2"}, {"${'$'}numberInt": "3"}],
444-
| {"a": {"${'$'}numberInt": "2"}}],
445-
| "arrayComplexMixedTypes": [{"a": {"${'$'}numberInt": "1"}}, {"a": "a"}],
446-
| "binary": {"${'$'}binary": {"base64": "S2Fma2Egcm9ja3Mh", "subType": "00"}},
447-
| "boolean": true,
448-
| "code": {"${'$'}code": "int i = 0;"},
449-
| "codeWithScope": {"${'$'}code": "int x = y", "${'$'}scope": {"y": {"${'$'}numberInt": "1"}}},
450-
| "dateTime": {"${'$'}date": {"${'$'}numberLong": "1577836801000"}},
451-
| "decimal128": {"${'$'}numberDecimal": "1.0"},
452-
| "documentEmpty": {},
453-
| "document": {"a": {"${'$'}numberInt": "1"}},
454-
| "double": {"${'$'}numberDouble": "62.0"},
455-
| "int32": {"${'$'}numberInt": "42"},
456-
| "int64": {"${'$'}numberLong": "52"},
457-
| "maxKey": {"${'$'}maxKey": 1},
458-
| "minKey": {"${'$'}minKey": 1},
459-
| "null": null,
460-
| "objectId": {"${'$'}oid": "5f3d1bbde0ca4d2829c91e1d"},
461-
| "regex": {"${'$'}regularExpression": {"pattern": "^test.*regex.*xyz$", "options": "i"}},
462-
| "string": "the fox ...",
463-
| "symbol": {"${'$'}symbol": "ruby stuff"},
464-
| "timestamp": {"${'$'}timestamp": {"t": 305419896, "i": 5}},
465-
| "undefined": {"${'$'}undefined": true}
466-
| }"""
467-
.trimMargin()
468-
val expected = """{"objectId": {"${'$'}oid": "111111111111111111111111"}, "bsonDocument": $subDocument}"""
472+
fun testDataClassBsonValues() {
469473

470474
val dataClass =
471-
DataClassWithObjectIdAndBsonDocument(ObjectId("111111111111111111111111"), BsonDocument.parse(subDocument))
472-
assertRoundTrips(expected, dataClass)
475+
DataClassBsonValues(
476+
allBsonTypesDocument["id"]!!.asObjectId().value,
477+
allBsonTypesDocument["arrayEmpty"]!!.asArray(),
478+
allBsonTypesDocument["arraySimple"]!!.asArray(),
479+
allBsonTypesDocument["arrayComplex"]!!.asArray(),
480+
allBsonTypesDocument["arrayMixedTypes"]!!.asArray(),
481+
allBsonTypesDocument["arrayComplexMixedTypes"]!!.asArray(),
482+
allBsonTypesDocument["binary"]!!.asBinary(),
483+
allBsonTypesDocument["boolean"]!!.asBoolean(),
484+
allBsonTypesDocument["code"]!!.asJavaScript(),
485+
allBsonTypesDocument["codeWithScope"]!!.asJavaScriptWithScope(),
486+
allBsonTypesDocument["dateTime"]!!.asDateTime(),
487+
allBsonTypesDocument["decimal128"]!!.asDecimal128(),
488+
allBsonTypesDocument["documentEmpty"]!!.asDocument(),
489+
allBsonTypesDocument["document"]!!.asDocument(),
490+
allBsonTypesDocument["double"]!!.asDouble(),
491+
allBsonTypesDocument["int32"]!!.asInt32(),
492+
allBsonTypesDocument["int64"]!!.asInt64(),
493+
allBsonTypesDocument["maxKey"]!! as BsonMaxKey,
494+
allBsonTypesDocument["minKey"]!! as BsonMinKey,
495+
allBsonTypesDocument["objectId"]!!.asObjectId(),
496+
allBsonTypesDocument["regex"]!!.asRegularExpression(),
497+
allBsonTypesDocument["string"]!!.asString(),
498+
allBsonTypesDocument["symbol"]!!.asSymbol(),
499+
allBsonTypesDocument["timestamp"]!!.asTimestamp(),
500+
allBsonTypesDocument["undefined"]!! as BsonUndefined)
501+
502+
assertRoundTrips(allBsonTypesJson, dataClass)
503+
}
504+
505+
@Test
506+
fun testDataClassOptionalBsonValues() {
507+
val dataClass =
508+
DataClassOptionalBsonValues(
509+
allBsonTypesDocument["id"]!!.asObjectId().value,
510+
allBsonTypesDocument["arrayEmpty"]!!.asArray(),
511+
allBsonTypesDocument["arraySimple"]!!.asArray(),
512+
allBsonTypesDocument["arrayComplex"]!!.asArray(),
513+
allBsonTypesDocument["arrayMixedTypes"]!!.asArray(),
514+
allBsonTypesDocument["arrayComplexMixedTypes"]!!.asArray(),
515+
allBsonTypesDocument["binary"]!!.asBinary(),
516+
allBsonTypesDocument["boolean"]!!.asBoolean(),
517+
allBsonTypesDocument["code"]!!.asJavaScript(),
518+
allBsonTypesDocument["codeWithScope"]!!.asJavaScriptWithScope(),
519+
allBsonTypesDocument["dateTime"]!!.asDateTime(),
520+
allBsonTypesDocument["decimal128"]!!.asDecimal128(),
521+
allBsonTypesDocument["documentEmpty"]!!.asDocument(),
522+
allBsonTypesDocument["document"]!!.asDocument(),
523+
allBsonTypesDocument["double"]!!.asDouble(),
524+
allBsonTypesDocument["int32"]!!.asInt32(),
525+
allBsonTypesDocument["int64"]!!.asInt64(),
526+
allBsonTypesDocument["maxKey"]!! as BsonMaxKey,
527+
allBsonTypesDocument["minKey"]!! as BsonMinKey,
528+
allBsonTypesDocument["objectId"]!!.asObjectId(),
529+
allBsonTypesDocument["regex"]!!.asRegularExpression(),
530+
allBsonTypesDocument["string"]!!.asString(),
531+
allBsonTypesDocument["symbol"]!!.asSymbol(),
532+
allBsonTypesDocument["timestamp"]!!.asTimestamp(),
533+
allBsonTypesDocument["undefined"]!! as BsonUndefined)
534+
535+
assertRoundTrips(allBsonTypesJson, dataClass)
536+
537+
val emptyDataClass =
538+
DataClassOptionalBsonValues(
539+
null,
540+
null,
541+
null,
542+
null,
543+
null,
544+
null,
545+
null,
546+
null,
547+
null,
548+
null,
549+
null,
550+
null,
551+
null,
552+
null,
553+
null,
554+
null,
555+
null,
556+
null,
557+
null,
558+
null,
559+
null,
560+
null,
561+
null,
562+
null,
563+
null)
564+
565+
assertRoundTrips("{}", emptyDataClass)
566+
assertRoundTrips(
567+
"""{ "id": null, "arrayEmpty": null, "arraySimple": null, "arrayComplex": null, "arrayMixedTypes": null,
568+
| "arrayComplexMixedTypes": null, "binary": null, "boolean": null, "code": null, "codeWithScope": null,
569+
| "dateTime": null, "decimal128": null, "documentEmpty": null, "document": null, "double": null,
570+
| "int32": null, "int64": null, "maxKey": null, "minKey": null, "objectId": null, "regex": null,
571+
| "string": null, "symbol": null, "timestamp": null, "undefined": null }"""
572+
.trimMargin(),
573+
emptyDataClass,
574+
BsonConfiguration(explicitNulls = true))
473575
}
474576

475577
@Test

0 commit comments

Comments
 (0)