Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ internal data class DataClassCodec<T : Any>(
@Suppress("TooGenericExceptionCaught")
override fun decode(reader: BsonReader, decoderContext: DecoderContext): T {
val args: MutableMap<KParameter, Any?> = mutableMapOf()
fieldNamePropertyModelMap.values.forEach { args[it.param] = null }

reader.readStartDocument()
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
Expand All @@ -89,6 +88,7 @@ internal data class DataClassCodec<T : Any>(
}
} else if (propertyModel.param.type.isMarkedNullable && reader.currentBsonType == BsonType.NULL) {
reader.readNull()
args[propertyModel.param] = null
} else {
try {
args[propertyModel.param] = decoderContext.decodeWithChildContext(propertyModel.codec, reader)
Expand All @@ -100,6 +100,19 @@ internal data class DataClassCodec<T : Any>(
}
reader.readEndDocument()

// For non-optional parameters missing from the document, fail with a clear message
// if non-nullable, or pass null explicitly if nullable.
// Optional parameters (with defaults) are left absent so callBy uses the default value.
fieldNamePropertyModelMap.values.forEach {
if (it.param !in args && !it.param.isOptional) {
if (!it.param.type.isMarkedNullable && it.param.type.classifier is KClass<*>) {
throw CodecConfigurationException(
"Required field '${it.fieldName}' is missing from the document for ${kClass.simpleName} data class")
}
Comment thread
rozza marked this conversation as resolved.
args[it.param] = null
Comment thread
rozza marked this conversation as resolved.
}
}

try {
return primaryConstructor.callBy(args)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import org.bson.codecs.kotlin.samples.DataClassWithBsonProperty
import org.bson.codecs.kotlin.samples.DataClassWithCollections
import org.bson.codecs.kotlin.samples.DataClassWithDataClassMapKey
import org.bson.codecs.kotlin.samples.DataClassWithDefaults
import org.bson.codecs.kotlin.samples.DataClassWithDefaultsAndNulls
import org.bson.codecs.kotlin.samples.DataClassWithEmbedded
import org.bson.codecs.kotlin.samples.DataClassWithEnum
import org.bson.codecs.kotlin.samples.DataClassWithEnumMapKey
Expand Down Expand Up @@ -177,17 +178,71 @@ class DataClassCodecTest {
|}"""
.trimMargin()

val defaultDataClass = DataClassWithDefaults()
assertRoundTrips(expectedDefault, defaultDataClass)
assertRoundTrips(expectedDefault, DataClassWithDefaults())

// Assert no data decodes as expected
assertDecodesTo(BsonDocument.parse(emptyDocument), DataClassWithDefaults())

// Assert some data
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithDefaults(string = "Custom"))

// Assert all data
val expected =
"""{
| "boolean": true,
| "string": "Custom",
| "listSimple": ["x"]
|}"""
.trimMargin()

assertRoundTrips(expected, DataClassWithDefaults(boolean = true, string = "Custom", listSimple = listOf("x")))
}

@Test
fun testDataClassWithNulls() {
val dataClass = DataClassWithNulls(null, null, null)
assertRoundTrips(emptyDocument, dataClass)

val withStoredNulls = BsonDocument.parse("""{"boolean": null, "string": null, "listSimple": null}""")
assertDecodesTo(withStoredNulls, dataClass)
// Assert all null data decodes as expected
assertDecodesTo(BsonDocument.parse("""{"boolean": null, "string": null, "listSimple": null}"""), dataClass)

// Assert some data
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithNulls(null, "Custom", null))

// Assert all data
val expected =
"""{
| "boolean": true,
| "string": "Custom",
| "listSimple": ["x"]
|}"""
.trimMargin()
assertRoundTrips(expected, DataClassWithNulls(true, "Custom", listOf("x")))
}

@Test
fun testDataClassWithDefaultsAndNulls() {
// All fields provided
val expected = """{"required": "req", "optional": "opt", "nullable": "nul"}"""
assertRoundTrips(expected, DataClassWithDefaultsAndNulls("req", "opt", "nul"))

// Only required field — optional gets default, nullable gets default (null)
assertDecodesTo(BsonDocument.parse("""{"required": "req"}"""), DataClassWithDefaultsAndNulls("req"))

// Required + nullable explicit null in document
assertDecodesTo(
BsonDocument.parse("""{"required": "req", "nullable": null}"""), DataClassWithDefaultsAndNulls("req"))

// Required + optional overridden, nullable absent
assertDecodesTo(
BsonDocument.parse("""{"required": "req", "optional": "custom"}"""),
DataClassWithDefaultsAndNulls("req", "custom"))

// Missing required field throws
assertThrows<CodecConfigurationException> {
val codec = DataClassCodec.create(DataClassWithDefaultsAndNulls::class, registry())
codec?.decode(BsonDocumentReader(BsonDocument()), DecoderContext.builder().build())
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ data class DataClassWithDefaults(

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

data class DataClassWithDefaultsAndNulls(
val required: String,
val optional: String = "default",
val nullable: String? = null
)

data class DataClassWithListThatLastItemDefaultsToNull(val elements: List<DataClassLastItemDefaultsToNull>)

data class DataClassLastItemDefaultsToNull(val required: String, val optional: String? = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithContextualDateValues
import org.bson.codecs.kotlinx.samples.DataClassWithDataClassMapKey
import org.bson.codecs.kotlinx.samples.DataClassWithDateValues
import org.bson.codecs.kotlinx.samples.DataClassWithDefaults
import org.bson.codecs.kotlinx.samples.DataClassWithDefaultsAndNulls
import org.bson.codecs.kotlinx.samples.DataClassWithEmbedded
import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault
import org.bson.codecs.kotlinx.samples.DataClassWithEnum
Expand Down Expand Up @@ -303,28 +304,71 @@ class KotlinSerializerCodecTest {
|}"""
.trimMargin()

val defaultDataClass = DataClassWithDefaults()
assertRoundTrips(expectedDefault, defaultDataClass)
assertRoundTrips(emptyDocument, defaultDataClass, altConfiguration)
assertRoundTrips(expectedDefault, DataClassWithDefaults())

val expectedSomeOverrides = """{"boolean": true, "listSimple": ["a"]}"""
val someOverridesDataClass = DataClassWithDefaults(boolean = true, listSimple = listOf("a"))
assertRoundTrips(expectedSomeOverrides, someOverridesDataClass, altConfiguration)
// Assert no data decodes as expected
assertDecodesTo(BsonDocument.parse(emptyDocument), DataClassWithDefaults())

// Assert some data
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithDefaults(string = "Custom"))

// Assert all data
val expected =
"""{
| "boolean": true,
| "string": "Custom",
| "listSimple": ["x"]
|}"""
.trimMargin()

assertRoundTrips(expected, DataClassWithDefaults(boolean = true, string = "Custom", listSimple = listOf("x")))
}

@Test
fun testDataClassWithNulls() {
val expectedNulls =
val dataClass = DataClassWithNulls(null, null, null)
assertRoundTrips(emptyDocument, dataClass)

// Assert all null data decodes as expected
assertDecodesTo(BsonDocument.parse("""{"boolean": null, "string": null, "listSimple": null}"""), dataClass)

// Assert some data
assertDecodesTo(BsonDocument.parse("""{"string": "Custom"}"""), DataClassWithNulls(null, "Custom", null))

// Assert all data
val expected =
"""{
| "boolean": null,
| "string": null,
| "listSimple": null
| "boolean": true,
| "string": "Custom",
| "listSimple": ["x"]
|}"""
.trimMargin()
assertRoundTrips(expected, DataClassWithNulls(true, "Custom", listOf("x")))
}

val dataClass = DataClassWithNulls(null, null, null)
assertRoundTrips(emptyDocument, dataClass)
assertRoundTrips(expectedNulls, dataClass, altConfiguration)
@Test
fun testDataClassWithDefaultsAndNulls() {
// All fields provided
val expected = """{"required": "req", "optional": "opt", "nullable": "nul"}"""
assertRoundTrips(expected, DataClassWithDefaultsAndNulls("req", "opt", "nul"))

// Only required field — optional gets default, nullable gets default (null)
assertDecodesTo(BsonDocument.parse("""{"required": "req"}"""), DataClassWithDefaultsAndNulls("req"))

// Required + nullable explicit null in document
assertDecodesTo(
BsonDocument.parse("""{"required": "req", "nullable": null}"""), DataClassWithDefaultsAndNulls("req"))

// Required + optional overridden, nullable absent
assertDecodesTo(
BsonDocument.parse("""{"required": "req", "optional": "custom"}"""),
DataClassWithDefaultsAndNulls("req", "custom"))

// Missing required field throws
assertThrows<MissingFieldException> {
val codec = KotlinSerializerCodec.create(DataClassWithDefaultsAndNulls::class)
codec?.decode(BsonDocumentReader(BsonDocument()), DecoderContext.builder().build())
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ data class DataClassWithKotlinAllowedName(

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

@Serializable
data class DataClassWithDefaultsAndNulls(
val required: String,
val optional: String = "default",
val nullable: String? = null
)

@Serializable
data class DataClassWithListThatLastItemDefaultsToNull(val elements: List<DataClassLastItemDefaultsToNull>)

Expand Down