diff --git a/bson/bsoncodec.go b/bson/bsoncodec.go index bacc99fbb7..46b7ae67a8 100644 --- a/bson/bsoncodec.go +++ b/bson/bsoncodec.go @@ -88,6 +88,7 @@ type EncodeContext struct { nilSliceAsEmpty bool nilByteSliceAsEmpty bool omitZeroStruct bool + omitZero bool useJSONStructTags bool } diff --git a/bson/default_value_decoders_test.go b/bson/default_value_decoders_test.go index 4dad538a26..499a516c33 100644 --- a/bson/default_value_decoders_test.go +++ b/bson/default_value_decoders_test.go @@ -2805,6 +2805,26 @@ func TestDefaultValueDecoders(t *testing.T) { buildDocument(bsoncore.AppendStringElement(nil, "a", "bar")), nil, }, + { + "omitzero", + struct { + A [4]int `bson:",omitzero"` + }{ + A: [4]int{}, + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "omitzero, empty time", + struct { + A time.Time `bson:",omitzero"` + }{ + A: time.Time{}, + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, { "struct{}", struct { diff --git a/bson/default_value_encoders_test.go b/bson/default_value_encoders_test.go index e15019785d..75966a7f79 100644 --- a/bson/default_value_encoders_test.go +++ b/bson/default_value_encoders_test.go @@ -1247,6 +1247,24 @@ func TestDefaultValueEncoders(t *testing.T) { []byte{0x05, 0x00, 0x00, 0x00, 0x00}, nil, }, + { + "omitzero", + struct { + A [4]int `bson:",omitzero"` + }{}, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, + { + "omitzero, empty time", + struct { + A time.Time `bson:",omitzero"` + }{ + A: time.Time{}, + }, + []byte{0x05, 0x00, 0x00, 0x00, 0x00}, + nil, + }, { "no private fields", noPrivateFields{a: "should be empty"}, diff --git a/bson/encoder.go b/bson/encoder.go index 0ad2432d12..9230c445d9 100644 --- a/bson/encoder.go +++ b/bson/encoder.go @@ -116,6 +116,11 @@ func (e *Encoder) OmitZeroStruct() { e.ec.omitZeroStruct = true } +// OmitZero causes the Encoder to omit zero values from the marshaled BSON. +func (e *Encoder) OmitZero() { + e.ec.omitZero = 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..e44034a413 100644 --- a/bson/encoder_test.go +++ b/bson/encoder_test.go @@ -248,6 +248,17 @@ func TestEncoderConfiguration(t *testing.T) { }{}, want: bsoncore.NewDocumentBuilder().Build(), }, + // Test that OmitZero omits zero values from the marshaled document. + { + description: "OmitZero", + configure: func(enc *Encoder) { + enc.OmitZero() + }, + input: struct { + Values [4]int + }{}, + 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..15b3919aa6 100644 --- a/bson/primitive_codecs_test.go +++ b/bson/primitive_codecs_test.go @@ -253,6 +253,46 @@ func TestPrimitiveValueEncoders(t *testing.T) { docToBytes(D{}), nil, }, + { + "omitzero map", + struct { + T map[string]string `bson:",omitzero"` + }{ + T: map[string]string{}, + }, + docToBytes(D{}), + nil, + }, + { + "omitzero slice", + struct { + T []struct{} `bson:",omitzero"` + }{ + T: []struct{}{}, + }, + docToBytes(D{}), + nil, + }, + { + "omitzero array", + struct { + T [4]int `bson:",omitzero"` + }{ + T: [4]int{}, + }, + docToBytes(D{}), + nil, + }, + { + "omitzero string", + struct { + T string `bson:",omitzero"` + }{ + T: "", + }, + docToBytes(D{}), + nil, + }, { "struct{}", struct { @@ -743,6 +783,26 @@ func TestPrimitiveValueDecoders(t *testing.T) { docToBytes(D{}), nil, }, + { + "omitzero", + struct { + A [4]int `bson:",omitzero"` + }{ + A: [4]int{}, + }, + docToBytes(D{}), + nil, + }, + { + "omitzero, empty time", + struct { + A time.Time `bson:",omitzero"` + }{ + A: time.Time{}, + }, + docToBytes(D{}), + nil, + }, { "no private fields", noPrivateFields{a: "should be empty"}, @@ -817,6 +877,18 @@ func TestPrimitiveValueDecoders(t *testing.T) { docToBytes(D{{"a", "bar"}}), nil, }, + { + "inline, omitzero", + struct { + A string + Foo zeroTest `bson:"omitzero,inline"` + }{ + A: "bar", + Foo: zeroTest{true}, + }, + docToBytes(D{{"a", "bar"}}), + nil, + }, { "JavaScript to D", D{{"a", JavaScript(`function() { var hello = "world"; }`)}}, diff --git a/bson/struct_codec.go b/bson/struct_codec.go index b3f160c8b6..be57e49d26 100644 --- a/bson/struct_codec.go +++ b/bson/struct_codec.go @@ -125,7 +125,7 @@ func (sc *structCodec) EncodeValue(ec EncodeContext, vw ValueWriter, val reflect } if errors.Is(err, errInvalidValue) { - if desc.omitEmpty { + if desc.omitEmpty || desc.omitZero { continue } vw2, err := dw.WriteDocumentElement(desc.name) @@ -145,6 +145,10 @@ func (sc *structCodec) EncodeValue(ec EncodeContext, vw ValueWriter, val reflect encoder := desc.encoder + if isZero(rv) && (desc.omitZero || ec.omitZero) { + continue + } + var empty bool if rv.Kind() == reflect.Interface { // isEmpty will not treat an interface rv as an interface, so we need to check for the @@ -171,6 +175,7 @@ func (sc *structCodec) EncodeValue(ec EncodeContext, vw ValueWriter, val reflect nilSliceAsEmpty: ec.nilSliceAsEmpty, nilByteSliceAsEmpty: ec.nilByteSliceAsEmpty, omitZeroStruct: ec.omitZeroStruct, + omitZero: ec.omitZero, useJSONStructTags: ec.useJSONStructTags, } err = encoder.EncodeValue(ectx, vw2, rv) @@ -361,6 +366,35 @@ func (sc *structCodec) DecodeValue(dc DecodeContext, vr ValueReader, val reflect return nil } +func isZero(v reflect.Value) bool { + kind := v.Kind() + if (kind != reflect.Ptr || !v.IsNil()) && v.Type().Implements(tZeroer) { + return v.Interface().(Zeroer).IsZero() + } + switch kind { + case reflect.Array: + n := v.Len() + for i := 0; i < n; i++ { + if !isZero(v.Index(i)) { + return false + } + } + case reflect.Struct: + vt := v.Type() + if vt == tTime { + return v.Interface().(time.Time).IsZero() + } + numField := vt.NumField() + for i := 0; i < numField; i++ { + if !isZero(v.Field(i)) { + return false + } + } + return true + } + return v.IsZero() +} + func isEmpty(v reflect.Value, omitZeroStruct bool) bool { kind := v.Kind() if (kind != reflect.Ptr || !v.IsNil()) && v.Type().Implements(tZeroer) { @@ -404,6 +438,7 @@ type fieldDescription struct { fieldName string // struct field name idx int omitEmpty bool + omitZero bool minSize bool truncate bool inline []int @@ -517,6 +552,7 @@ func (sc *structCodec) describeStructSlow( } description.name = stags.Name description.omitEmpty = stags.OmitEmpty + description.omitZero = stags.OmitZero description.minSize = stags.MinSize description.truncate = stags.Truncate diff --git a/bson/struct_tag_parser.go b/bson/struct_tag_parser.go index 47955639b4..65dd6b20b6 100644 --- a/bson/struct_tag_parser.go +++ b/bson/struct_tag_parser.go @@ -38,6 +38,7 @@ import ( type structTags struct { Name string OmitEmpty bool + OmitZero bool MinSize bool Truncate bool Inline bool @@ -108,6 +109,8 @@ func parseTags(key string, tag string) (*structTags, error) { switch str { case "omitempty": st.OmitEmpty = true + case "omitzero": + st.OmitZero = true case "minsize": st.MinSize = true case "truncate": diff --git a/bson/struct_tag_parser_test.go b/bson/struct_tag_parser_test.go index 3592761e89..9d4657a065 100644 --- a/bson/struct_tag_parser_test.go +++ b/bson/struct_tag_parser_test.go @@ -46,26 +46,26 @@ func TestStructTagParsers(t *testing.T) { }, { "default all options", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)}, - &structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,omitzero,minsize,truncate,inline`)}, + &structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseStructTags, }, { "default all options default name", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)}, - &structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,omitzero,minsize,truncate,inline`)}, + &structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseStructTags, }, { "default bson tag all options", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)}, - &structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,omitzero,minsize,truncate,inline"`)}, + &structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseStructTags, }, { "default bson tag all options default name", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)}, - &structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,omitzero,minsize,truncate,inline"`)}, + &structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseStructTags, }, { @@ -100,38 +100,38 @@ func TestStructTagParsers(t *testing.T) { }, { "JSONFallback all options", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)}, - &structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,omitzero,minsize,truncate,inline`)}, + &structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseJSONStructTags, }, { "JSONFallback all options default name", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)}, - &structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,omitzero,minsize,truncate,inline`)}, + &structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseJSONStructTags, }, { "JSONFallback bson tag all options", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)}, - &structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,omitzero,minsize,truncate,inline"`)}, + &structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseJSONStructTags, }, { "JSONFallback bson tag all options default name", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)}, - &structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,omitzero,minsize,truncate,inline"`)}, + &structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseJSONStructTags, }, { "JSONFallback json tag all options", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:"bar,omitempty,minsize,truncate,inline"`)}, - &structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:"bar,omitempty,omitzero,minsize,truncate,inline"`)}, + &structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseJSONStructTags, }, { "JSONFallback json tag all options default name", - reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:",omitempty,minsize,truncate,inline"`)}, - &structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true}, + reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:",omitempty,omitzero,minsize,truncate,inline"`)}, + &structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true}, parseJSONStructTags, }, { diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index 6f18d9f146..09bb171a6d 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -956,6 +956,18 @@ func TestClient_BSONOptions(t *testing.T) { want: &bson.D{}, wantRaw: bson.Raw(bsoncore.NewDocumentBuilder().Build()), }, + { + name: "OmitZero", + bsonOpts: &options.BSONOptions{ + OmitZero: true, + }, + doc: struct { + X [4]int + }{}, + 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..02dc8c1ed4 100644 --- a/mongo/mongo.go +++ b/mongo/mongo.go @@ -83,6 +83,9 @@ func getEncoder( if opts.OmitZeroStruct { enc.OmitZeroStruct() } + if opts.OmitZero { + enc.OmitZero() + } if opts.StringifyMapKeysWithFmt { enc.StringifyMapKeysWithFmt() } diff --git a/mongo/mongo_test.go b/mongo/mongo_test.go index 96be905cb5..99884295aa 100644 --- a/mongo/mongo_test.go +++ b/mongo/mongo_test.go @@ -563,6 +563,7 @@ func TestMarshalValue(t *testing.T) { NilMap map[string]interface{} NilStrings []string ZeroStruct struct{ X int } `bson:"_,omitempty"` + ZeroArray [4]int StringerMap map[*bson.RawValue]bool BSONField string `json:"jsonField"` }{ @@ -578,6 +579,7 @@ func TestMarshalValue(t *testing.T) { NilMapAsEmpty: true, NilSliceAsEmpty: true, OmitZeroStruct: true, + OmitZero: true, StringifyMapKeysWithFmt: true, UseJSONStructTags: true, }, diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 9623562ec8..b25c5cff7b 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -185,6 +185,9 @@ type BSONOptions struct { // "omitempty" struct tag option is set. OmitZeroStruct bool + // OmitZero causes the driver to omit zero values from the marshaled BSON. + OmitZero 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.