diff --git a/bson/bsoncodec.go b/bson/bsoncodec.go index bacc99fbb7..80e13e7d81 100644 --- a/bson/bsoncodec.go +++ b/bson/bsoncodec.go @@ -88,6 +88,7 @@ type EncodeContext struct { nilSliceAsEmpty bool nilByteSliceAsEmpty bool omitZeroStruct bool + omitEmpty bool useJSONStructTags bool } diff --git a/bson/encoder.go b/bson/encoder.go index 0ad2432d12..8299e94f5f 100644 --- a/bson/encoder.go +++ b/bson/encoder.go @@ -108,7 +108,8 @@ func (e *Encoder) NilByteSliceAsEmpty() { // TODO struct fields once the logic is updated to also inspect private struct fields. // OmitZeroStruct causes the Encoder to consider the zero value for a struct (e.g. MyStruct{}) -// as empty and omit it from the marshaled BSON when the "omitempty" struct tag option is set. +// as empty and omit it from the marshaled BSON when the "omitempty" struct tag option is set +// or the OmitEmpty() method is called. // // Note that the Encoder only examines exported struct fields when determining if a struct is the // zero value. It considers pointers to a zero struct value (e.g. &MyStruct{}) not empty. @@ -116,6 +117,12 @@ func (e *Encoder) OmitZeroStruct() { e.ec.omitZeroStruct = true } +// OmitEmpty causes the Encoder to omit empty values from the marshaled BSON as the "omitempty" +// struct tag option is set. +func (e *Encoder) OmitEmpty() { + e.ec.omitEmpty = true +} + // UseJSONStructTags causes the Encoder to fall back to using the "json" struct tag if a "bson" // struct tag is not specified. func (e *Encoder) UseJSONStructTags() { diff --git a/bson/encoder_test.go b/bson/encoder_test.go index 997f58582b..af26617204 100644 --- a/bson/encoder_test.go +++ b/bson/encoder_test.go @@ -11,6 +11,7 @@ import ( "errors" "reflect" "testing" + "time" "go.mongodb.org/mongo-driver/v2/internal/assert" "go.mongodb.org/mongo-driver/v2/internal/require" @@ -248,6 +249,42 @@ func TestEncoderConfiguration(t *testing.T) { }{}, want: bsoncore.NewDocumentBuilder().Build(), }, + // Test that OmitZeroStruct omits empty structs from the marshaled document if + // OmitEmpty is also set. + { + description: "OmitEmpty with non-zeroer struct", + configure: func(enc *Encoder) { + enc.OmitZeroStruct() + enc.OmitEmpty() + }, + input: struct { + Zero zeroStruct + }{}, + want: bsoncore.NewDocumentBuilder().Build(), + }, + // Test that OmitEmpty omits empty values from the marshaled document. + { + description: "OmitEmpty", + configure: func(enc *Encoder) { + enc.OmitEmpty() + }, + input: struct { + Zero zeroTest + I64 int64 + F64 float64 + String string + Boolean bool + Slice []int + Array [0]int + Map map[string]int + Bytes []byte + Time time.Time + Pointer *int + }{ + Zero: zeroTest{true}, + }, + want: bsoncore.NewDocumentBuilder().Build(), + }, // Test that UseJSONStructTags causes the Encoder to fall back to "json" struct tags if // "bson" struct tags are not available. { diff --git a/bson/primitive_codecs_test.go b/bson/primitive_codecs_test.go index 684159efdf..d7480f9eaa 100644 --- a/bson/primitive_codecs_test.go +++ b/bson/primitive_codecs_test.go @@ -1094,6 +1094,8 @@ type zeroTest struct { func (z zeroTest) IsZero() bool { return z.reportZero } +var _ Zeroer = zeroTest{} + func compareZeroTest(_, _ zeroTest) bool { return true } func compareDecimal128(d1, d2 Decimal128) bool { diff --git a/bson/struct_codec.go b/bson/struct_codec.go index b3f160c8b6..83b63f5f67 100644 --- a/bson/struct_codec.go +++ b/bson/struct_codec.go @@ -118,6 +118,10 @@ func (sc *structCodec) EncodeValue(ec EncodeContext, vw ValueWriter, val reflect } } + if ec.omitEmpty { + desc.omitEmpty = true + } + desc.encoder, rv, err = lookupElementEncoder(ec, desc.encoder, rv) if err != nil && !errors.Is(err, errInvalidValue) { diff --git a/bson/struct_codec_test.go b/bson/struct_codec_test.go index e434079e60..cae19c7a01 100644 --- a/bson/struct_codec_test.go +++ b/bson/struct_codec_test.go @@ -14,16 +14,6 @@ import ( "go.mongodb.org/mongo-driver/v2/internal/assert" ) -var _ Zeroer = testZeroer{} - -type testZeroer struct { - val int -} - -func (z testZeroer) IsZero() bool { - return z.val != 0 -} - func TestIsZero(t *testing.T) { t.Parallel() testCases := []struct { @@ -84,22 +74,22 @@ func TestIsZero(t *testing.T) { }, { description: "zero struct that implements Zeroer", - value: testZeroer{}, + value: zeroTest{}, want: false, }, { description: "non-zero struct that implements Zeroer", - value: &testZeroer{val: 1}, + value: zeroTest{reportZero: true}, want: true, }, { description: "pointer to zero struct that implements Zeroer", - value: &testZeroer{}, + value: &zeroTest{}, want: false, }, { description: "pointer to non-zero struct that implements Zeroer", - value: testZeroer{val: 1}, + value: &zeroTest{reportZero: true}, want: true, }, { diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index 6f18d9f146..0478967a52 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -956,6 +956,19 @@ func TestClient_BSONOptions(t *testing.T) { want: &bson.D{}, wantRaw: bson.Raw(bsoncore.NewDocumentBuilder().Build()), }, + { + name: "OmitEmpty with non-zeroer struct", + bsonOpts: &options.BSONOptions{ + OmitZeroStruct: true, + OmitEmpty: true, + }, + doc: struct { + X jsonTagsTest `bson:"x"` + }{}, + decodeInto: func() interface{} { return &bson.D{} }, + want: &bson.D{}, + wantRaw: bson.Raw(bsoncore.NewDocumentBuilder().Build()), + }, { name: "StringifyMapKeysWithFmt", bsonOpts: &options.BSONOptions{ diff --git a/mongo/mongo.go b/mongo/mongo.go index 260576bd1e..b40ce15c07 100644 --- a/mongo/mongo.go +++ b/mongo/mongo.go @@ -83,6 +83,9 @@ func getEncoder( if opts.OmitZeroStruct { enc.OmitZeroStruct() } + if opts.OmitEmpty { + enc.OmitEmpty() + } if opts.StringifyMapKeysWithFmt { enc.StringifyMapKeysWithFmt() } diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 9623562ec8..304c69835d 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -182,9 +182,12 @@ type BSONOptions struct { // OmitZeroStruct causes the driver to consider the zero value for a struct // (e.g. MyStruct{}) as empty and omit it from the marshaled BSON when the - // "omitempty" struct tag option is set. + // "omitempty" struct tag option or the "OmitEmpty" field is set. OmitZeroStruct bool + // OmitEmpty causes the driver to omit empty values from the marshaled BSON. + OmitEmpty bool + // StringifyMapKeysWithFmt causes the driver to convert Go map keys to BSON // document field name strings using fmt.Sprint instead of the default // string conversion logic.