diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt index 40abc3a9cfa..e3cfe530705 100644 --- a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt @@ -25,6 +25,7 @@ import org.bson.codecs.configuration.CodecConfigurationException import org.bson.codecs.configuration.CodecRegistries.fromProviders import org.bson.codecs.kotlin.samples.Box import org.bson.codecs.kotlin.samples.DataClassEmbedded +import org.bson.codecs.kotlin.samples.DataClassLastItemDefaultsToNull import org.bson.codecs.kotlin.samples.DataClassListOfDataClasses import org.bson.codecs.kotlin.samples.DataClassListOfListOfDataClasses import org.bson.codecs.kotlin.samples.DataClassListOfSealed @@ -51,6 +52,7 @@ import org.bson.codecs.kotlin.samples.DataClassWithEnum import org.bson.codecs.kotlin.samples.DataClassWithEnumMapKey import org.bson.codecs.kotlin.samples.DataClassWithFailingInit import org.bson.codecs.kotlin.samples.DataClassWithInvalidBsonRepresentation +import org.bson.codecs.kotlin.samples.DataClassWithListThatLastItemDefaultsToNull import org.bson.codecs.kotlin.samples.DataClassWithMutableList import org.bson.codecs.kotlin.samples.DataClassWithMutableMap import org.bson.codecs.kotlin.samples.DataClassWithMutableSet @@ -133,6 +135,20 @@ class DataClassCodecTest { assertDecodesTo(withStoredNulls, dataClass) } + @Test + fun testDataClassWithListThatLastItemDefaultsToNull() { + val expected = + """{ + | "elements": [{"required": "required"}, {"required": "required"}], + |}""" + .trimMargin() + + val dataClass = + DataClassWithListThatLastItemDefaultsToNull( + listOf(DataClassLastItemDefaultsToNull("required"), DataClassLastItemDefaultsToNull("required"))) + assertRoundTrips(expected, dataClass) + } + @Test fun testDataClassWithNullableGenericsNotNull() { val expected = diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt index 5bc6e768ed8..aa2c8983b1d 100644 --- a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt @@ -57,6 +57,10 @@ data class DataClassWithDefaults( data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List?) +data class DataClassWithListThatLastItemDefaultsToNull(val elements: List) + +data class DataClassLastItemDefaultsToNull(val required: String, val optional: String? = null) + data class DataClassSelfReferential( val name: String, val left: DataClassSelfReferential? = null, diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt index b3ae0c8cdf4..75080254cdb 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt @@ -72,7 +72,7 @@ internal class DefaultBsonEncoder( private var isPolymorphic = false private var state = STATE.VALUE private var mapState = MapState() - private var deferredElementName: String? = null + private val deferredElementHandler: DeferredElementHandler = DeferredElementHandler() override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = configuration.encodeDefaults @@ -117,7 +117,7 @@ internal class DefaultBsonEncoder( is StructureKind.CLASS -> { val elementName = descriptor.getElementName(index) if (descriptor.getElementDescriptor(index).isNullable) { - deferredElementName = elementName + deferredElementHandler.set(elementName) } else { encodeName(elementName) } @@ -140,25 +140,27 @@ internal class DefaultBsonEncoder( } override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { - deferredElementName?.let { - if (value != null || configuration.explicitNulls) { - encodeName(it) - super.encodeSerializableValue(serializer, value) - } else { - deferredElementName = null - } - } - ?: super.encodeSerializableValue(serializer, value) + deferredElementHandler.with( + { + // When using generics its possible for `value` to be null + // See: https://youtrack.jetbrains.com/issue/KT-66206 + if (value != null || configuration.explicitNulls) { + encodeName(it) + super.encodeSerializableValue(serializer, value) + } + }, + { super.encodeSerializableValue(serializer, value) }) } override fun encodeNullableSerializableValue(serializer: SerializationStrategy, value: T?) { - deferredElementName?.let { - if (value != null || configuration.explicitNulls) { - encodeName(it) - super.encodeNullableSerializableValue(serializer, value) - } - } - ?: super.encodeNullableSerializableValue(serializer, value) + deferredElementHandler.with( + { + if (value != null || configuration.explicitNulls) { + encodeName(it) + super.encodeNullableSerializableValue(serializer, value) + } + }, + { super.encodeNullableSerializableValue(serializer, value) }) } override fun encodeByte(value: Byte) = encodeInt(value.toInt()) @@ -170,14 +172,7 @@ internal class DefaultBsonEncoder( override fun encodeDouble(value: Double) = writer.writeDouble(value) override fun encodeInt(value: Int) = writer.writeInt32(value) override fun encodeLong(value: Long) = writer.writeInt64(value) - override fun encodeNull() { - deferredElementName?.let { - if (configuration.explicitNulls) { - encodeName(it) - } - } - writer.writeNull() - } + override fun encodeNull() = writer.writeNull() override fun encodeString(value: String) { when (state) { @@ -206,7 +201,6 @@ internal class DefaultBsonEncoder( private fun encodeName(value: Any) { writer.writeName(value.toString()) - deferredElementName = null state = STATE.VALUE } @@ -229,4 +223,25 @@ internal class DefaultBsonEncoder( return getState() } } + + private class DeferredElementHandler { + private var deferredElementName: String? = null + + fun set(name: String) { + assert(deferredElementName == null) { -> "Overwriting an existing deferred name" } + deferredElementName = name + } + + fun with(actionWithDeferredElement: (String) -> Unit, actionWithoutDeferredElement: () -> Unit): Unit { + deferredElementName?.let { + reset() + actionWithDeferredElement(it) + } + ?: actionWithoutDeferredElement() + } + + private fun reset() { + deferredElementName = null + } + } } 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 ed9e1bfb43a..05a0d3ffd7d 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 @@ -45,6 +45,7 @@ import org.bson.codecs.kotlinx.samples.DataClassContainsOpen import org.bson.codecs.kotlinx.samples.DataClassContainsValueClass import org.bson.codecs.kotlinx.samples.DataClassEmbedded import org.bson.codecs.kotlinx.samples.DataClassKey +import org.bson.codecs.kotlinx.samples.DataClassLastItemDefaultsToNull import org.bson.codecs.kotlinx.samples.DataClassListOfDataClasses import org.bson.codecs.kotlinx.samples.DataClassListOfListOfDataClasses import org.bson.codecs.kotlinx.samples.DataClassListOfSealed @@ -78,6 +79,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault import org.bson.codecs.kotlinx.samples.DataClassWithEnum import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit +import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull import org.bson.codecs.kotlinx.samples.DataClassWithMutableList import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet @@ -255,6 +257,27 @@ class KotlinSerializerCodecTest { assertRoundTrips(expectedNulls, dataClass, altConfiguration) } + @Test + fun testDataClassWithListThatLastItemDefaultsToNull() { + val expectedWithOutNulls = + """{ + | "elements": [{"required": "required"}, {"required": "required"}], + |}""" + .trimMargin() + + val dataClass = + DataClassWithListThatLastItemDefaultsToNull( + listOf(DataClassLastItemDefaultsToNull("required"), DataClassLastItemDefaultsToNull("required"))) + assertRoundTrips(expectedWithOutNulls, dataClass) + + val expectedWithNulls = + """{ + | "elements": [{"required": "required", "optional": null}, {"required": "required", "optional": null}], + |}""" + .trimMargin() + assertRoundTrips(expectedWithNulls, dataClass, BsonConfiguration(explicitNulls = true)) + } + @Test fun testDataClassWithNullableGenericsNotNull() { val expected = diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt index 2511c7b0418..66907bff103 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt @@ -82,6 +82,11 @@ data class DataClassWithDefaults( @Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List?) +@Serializable +data class DataClassWithListThatLastItemDefaultsToNull(val elements: List) + +@Serializable data class DataClassLastItemDefaultsToNull(val required: String, val optional: String? = null) + @Serializable data class DataClassSelfReferential( val name: String,