Skip to content

Commit ab72460

Browse files
authored
Fix bson-kotlinx encodeNullableSerializableValue null handling (#1453)
Ensures that the deferredElement name is reset correctly. Test case ported to bson-kotlin JAVA-5524
1 parent cd297a1 commit ab72460

File tree

5 files changed

+90
-27
lines changed

5 files changed

+90
-27
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.bson.codecs.configuration.CodecConfigurationException
2525
import org.bson.codecs.configuration.CodecRegistries.fromProviders
2626
import org.bson.codecs.kotlin.samples.Box
2727
import org.bson.codecs.kotlin.samples.DataClassEmbedded
28+
import org.bson.codecs.kotlin.samples.DataClassLastItemDefaultsToNull
2829
import org.bson.codecs.kotlin.samples.DataClassListOfDataClasses
2930
import org.bson.codecs.kotlin.samples.DataClassListOfListOfDataClasses
3031
import org.bson.codecs.kotlin.samples.DataClassListOfSealed
@@ -51,6 +52,7 @@ import org.bson.codecs.kotlin.samples.DataClassWithEnum
5152
import org.bson.codecs.kotlin.samples.DataClassWithEnumMapKey
5253
import org.bson.codecs.kotlin.samples.DataClassWithFailingInit
5354
import org.bson.codecs.kotlin.samples.DataClassWithInvalidBsonRepresentation
55+
import org.bson.codecs.kotlin.samples.DataClassWithListThatLastItemDefaultsToNull
5456
import org.bson.codecs.kotlin.samples.DataClassWithMutableList
5557
import org.bson.codecs.kotlin.samples.DataClassWithMutableMap
5658
import org.bson.codecs.kotlin.samples.DataClassWithMutableSet
@@ -133,6 +135,20 @@ class DataClassCodecTest {
133135
assertDecodesTo(withStoredNulls, dataClass)
134136
}
135137

138+
@Test
139+
fun testDataClassWithListThatLastItemDefaultsToNull() {
140+
val expected =
141+
"""{
142+
| "elements": [{"required": "required"}, {"required": "required"}],
143+
|}"""
144+
.trimMargin()
145+
146+
val dataClass =
147+
DataClassWithListThatLastItemDefaultsToNull(
148+
listOf(DataClassLastItemDefaultsToNull("required"), DataClassLastItemDefaultsToNull("required")))
149+
assertRoundTrips(expected, dataClass)
150+
}
151+
136152
@Test
137153
fun testDataClassWithNullableGenericsNotNull() {
138154
val expected =

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ data class DataClassWithDefaults(
5757

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

60+
data class DataClassWithListThatLastItemDefaultsToNull(val elements: List<DataClassLastItemDefaultsToNull>)
61+
62+
data class DataClassLastItemDefaultsToNull(val required: String, val optional: String? = null)
63+
6064
data class DataClassSelfReferential(
6165
val name: String,
6266
val left: DataClassSelfReferential? = null,

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

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ internal class DefaultBsonEncoder(
7272
private var isPolymorphic = false
7373
private var state = STATE.VALUE
7474
private var mapState = MapState()
75-
private var deferredElementName: String? = null
75+
private val deferredElementHandler: DeferredElementHandler = DeferredElementHandler()
7676

7777
override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean =
7878
configuration.encodeDefaults
@@ -117,7 +117,7 @@ internal class DefaultBsonEncoder(
117117
is StructureKind.CLASS -> {
118118
val elementName = descriptor.getElementName(index)
119119
if (descriptor.getElementDescriptor(index).isNullable) {
120-
deferredElementName = elementName
120+
deferredElementHandler.set(elementName)
121121
} else {
122122
encodeName(elementName)
123123
}
@@ -140,25 +140,27 @@ internal class DefaultBsonEncoder(
140140
}
141141

142142
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
143-
deferredElementName?.let {
144-
if (value != null || configuration.explicitNulls) {
145-
encodeName(it)
146-
super.encodeSerializableValue(serializer, value)
147-
} else {
148-
deferredElementName = null
149-
}
150-
}
151-
?: super.encodeSerializableValue(serializer, value)
143+
deferredElementHandler.with(
144+
{
145+
// When using generics its possible for `value` to be null
146+
// See: https://youtrack.jetbrains.com/issue/KT-66206
147+
if (value != null || configuration.explicitNulls) {
148+
encodeName(it)
149+
super.encodeSerializableValue(serializer, value)
150+
}
151+
},
152+
{ super.encodeSerializableValue(serializer, value) })
152153
}
153154

154155
override fun <T : Any> encodeNullableSerializableValue(serializer: SerializationStrategy<T>, value: T?) {
155-
deferredElementName?.let {
156-
if (value != null || configuration.explicitNulls) {
157-
encodeName(it)
158-
super.encodeNullableSerializableValue(serializer, value)
159-
}
160-
}
161-
?: super.encodeNullableSerializableValue(serializer, value)
156+
deferredElementHandler.with(
157+
{
158+
if (value != null || configuration.explicitNulls) {
159+
encodeName(it)
160+
super.encodeNullableSerializableValue(serializer, value)
161+
}
162+
},
163+
{ super.encodeNullableSerializableValue(serializer, value) })
162164
}
163165

164166
override fun encodeByte(value: Byte) = encodeInt(value.toInt())
@@ -170,14 +172,7 @@ internal class DefaultBsonEncoder(
170172
override fun encodeDouble(value: Double) = writer.writeDouble(value)
171173
override fun encodeInt(value: Int) = writer.writeInt32(value)
172174
override fun encodeLong(value: Long) = writer.writeInt64(value)
173-
override fun encodeNull() {
174-
deferredElementName?.let {
175-
if (configuration.explicitNulls) {
176-
encodeName(it)
177-
}
178-
}
179-
writer.writeNull()
180-
}
175+
override fun encodeNull() = writer.writeNull()
181176

182177
override fun encodeString(value: String) {
183178
when (state) {
@@ -206,7 +201,6 @@ internal class DefaultBsonEncoder(
206201

207202
private fun encodeName(value: Any) {
208203
writer.writeName(value.toString())
209-
deferredElementName = null
210204
state = STATE.VALUE
211205
}
212206

@@ -229,4 +223,25 @@ internal class DefaultBsonEncoder(
229223
return getState()
230224
}
231225
}
226+
227+
private class DeferredElementHandler {
228+
private var deferredElementName: String? = null
229+
230+
fun set(name: String) {
231+
assert(deferredElementName == null) { -> "Overwriting an existing deferred name" }
232+
deferredElementName = name
233+
}
234+
235+
fun with(actionWithDeferredElement: (String) -> Unit, actionWithoutDeferredElement: () -> Unit): Unit {
236+
deferredElementName?.let {
237+
reset()
238+
actionWithDeferredElement(it)
239+
}
240+
?: actionWithoutDeferredElement()
241+
}
242+
243+
private fun reset() {
244+
deferredElementName = null
245+
}
246+
}
232247
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import org.bson.codecs.kotlinx.samples.DataClassContainsOpen
4545
import org.bson.codecs.kotlinx.samples.DataClassContainsValueClass
4646
import org.bson.codecs.kotlinx.samples.DataClassEmbedded
4747
import org.bson.codecs.kotlinx.samples.DataClassKey
48+
import org.bson.codecs.kotlinx.samples.DataClassLastItemDefaultsToNull
4849
import org.bson.codecs.kotlinx.samples.DataClassListOfDataClasses
4950
import org.bson.codecs.kotlinx.samples.DataClassListOfListOfDataClasses
5051
import org.bson.codecs.kotlinx.samples.DataClassListOfSealed
@@ -78,6 +79,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault
7879
import org.bson.codecs.kotlinx.samples.DataClassWithEnum
7980
import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey
8081
import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit
82+
import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull
8183
import org.bson.codecs.kotlinx.samples.DataClassWithMutableList
8284
import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap
8385
import org.bson.codecs.kotlinx.samples.DataClassWithMutableSet
@@ -255,6 +257,27 @@ class KotlinSerializerCodecTest {
255257
assertRoundTrips(expectedNulls, dataClass, altConfiguration)
256258
}
257259

260+
@Test
261+
fun testDataClassWithListThatLastItemDefaultsToNull() {
262+
val expectedWithOutNulls =
263+
"""{
264+
| "elements": [{"required": "required"}, {"required": "required"}],
265+
|}"""
266+
.trimMargin()
267+
268+
val dataClass =
269+
DataClassWithListThatLastItemDefaultsToNull(
270+
listOf(DataClassLastItemDefaultsToNull("required"), DataClassLastItemDefaultsToNull("required")))
271+
assertRoundTrips(expectedWithOutNulls, dataClass)
272+
273+
val expectedWithNulls =
274+
"""{
275+
| "elements": [{"required": "required", "optional": null}, {"required": "required", "optional": null}],
276+
|}"""
277+
.trimMargin()
278+
assertRoundTrips(expectedWithNulls, dataClass, BsonConfiguration(explicitNulls = true))
279+
}
280+
258281
@Test
259282
fun testDataClassWithNullableGenericsNotNull() {
260283
val expected =

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ data class DataClassWithDefaults(
8282

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

85+
@Serializable
86+
data class DataClassWithListThatLastItemDefaultsToNull(val elements: List<DataClassLastItemDefaultsToNull>)
87+
88+
@Serializable data class DataClassLastItemDefaultsToNull(val required: String, val optional: String? = null)
89+
8590
@Serializable
8691
data class DataClassSelfReferential(
8792
val name: String,

0 commit comments

Comments
 (0)