diff --git a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java index 8a304760f31..5bce0560233 100644 --- a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java +++ b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java @@ -16,6 +16,7 @@ package org.bson.codecs.record; +import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; import org.bson.BsonType; import org.bson.BsonWriter; @@ -62,6 +63,7 @@ private static final class ComponentModel { private final Codec codec; private final int index; private final String fieldName; + private final boolean isNullable; private ComponentModel(final List typeParameters, final RecordComponent component, final CodecRegistry codecRegistry, final int index) { @@ -70,6 +72,7 @@ private ComponentModel(final List typeParameters, final RecordComponent co this.codec = computeCodec(typeParameters, component, codecRegistry); this.index = index; this.fieldName = computeFieldName(component); + this.isNullable = !component.getType().isPrimitive(); } String getComponentName() { @@ -275,6 +278,11 @@ public T decode(final BsonReader reader, final DecoderContext decoderContext) { if (LOGGER.isTraceEnabled()) { LOGGER.trace(format("Found property not present in the ClassModel: %s", fieldName)); } + } else if (reader.getCurrentBsonType() == BsonType.NULL) { + if (!componentModel.isNullable) { + throw new BsonInvalidOperationException(format("Null value on primitive field: %s", componentModel.fieldName)); + } + reader.readNull(); } else { constructorArguments[componentModel.index] = decoderContext.decodeWithChildContext(componentModel.codec, reader); } diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java index 606bc68e59a..636554443fd 100644 --- a/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java @@ -22,6 +22,8 @@ import org.bson.BsonDocumentWriter; import org.bson.BsonDouble; import org.bson.BsonInt32; +import org.bson.BsonInvalidOperationException; +import org.bson.BsonNull; import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.codecs.DecoderContext; @@ -49,6 +51,7 @@ import org.bson.codecs.record.samples.TestRecordWithMapOfRecords; import org.bson.codecs.record.samples.TestRecordWithNestedParameterized; import org.bson.codecs.record.samples.TestRecordWithNestedParameterizedRecord; +import org.bson.codecs.record.samples.TestRecordWithNullableField; import org.bson.codecs.record.samples.TestRecordWithParameterizedRecord; import org.bson.codecs.record.samples.TestRecordWithPojoAnnotations; import org.bson.codecs.record.samples.TestSelfReferentialHolderRecord; @@ -325,6 +328,35 @@ public void testRecordWithNulls() { assertEquals(testRecord, decoded); } + @Test + public void testRecordWithStoredNulls() { + var codec = createRecordCodec(TestRecordWithNullableField.class, Bson.DEFAULT_CODEC_REGISTRY); + var identifier = new ObjectId(); + var testRecord = new TestRecordWithNullableField(identifier, null, 42); + + var document = new BsonDocument("_id", new BsonObjectId(identifier)) + .append("name", new BsonNull()) + .append("age", new BsonInt32(42)); + + // when + var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build()); + + // then + assertEquals(testRecord, decoded); + } + + @Test + public void testExceptionsWithStoredNullsOnPrimitiveField() { + var codec = createRecordCodec(TestRecordWithNullableField.class, Bson.DEFAULT_CODEC_REGISTRY); + + var document = new BsonDocument("_id", new BsonObjectId(new ObjectId())) + .append("name", new BsonString("Felix")) + .append("age", new BsonNull()); + + assertThrows(BsonInvalidOperationException.class, () -> + codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build())); + } + @Test public void testRecordWithExtraData() { var codec = createRecordCodec(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithNullableField.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithNullableField.java new file mode 100644 index 00000000000..f2329c8170e --- /dev/null +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithNullableField.java @@ -0,0 +1,23 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs.record.samples; + +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +public record TestRecordWithNullableField(@BsonId ObjectId id, String name, int age) { +}