Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions bson/bsoncodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type EncodeContext struct {
nilSliceAsEmpty bool
nilByteSliceAsEmpty bool
omitZeroStruct bool
omitZero bool
useJSONStructTags bool
}

Expand Down
20 changes: 20 additions & 0 deletions bson/default_value_decoders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions bson/default_value_encoders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
5 changes: 5 additions & 0 deletions bson/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
11 changes: 11 additions & 0 deletions bson/encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
{
Expand Down
72 changes: 72 additions & 0 deletions bson/primitive_codecs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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"; }`)}},
Expand Down
38 changes: 37 additions & 1 deletion bson/struct_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -404,6 +438,7 @@ type fieldDescription struct {
fieldName string // struct field name
idx int
omitEmpty bool
omitZero bool
minSize bool
truncate bool
inline []int
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions bson/struct_tag_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
type structTags struct {
Name string
OmitEmpty bool
OmitZero bool
MinSize bool
Truncate bool
Inline bool
Expand Down Expand Up @@ -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":
Expand Down
40 changes: 20 additions & 20 deletions bson/struct_tag_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand Down Expand Up @@ -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,
},
{
Expand Down
12 changes: 12 additions & 0 deletions internal/integration/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
3 changes: 3 additions & 0 deletions mongo/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func getEncoder(
if opts.OmitZeroStruct {
enc.OmitZeroStruct()
}
if opts.OmitZero {
enc.OmitZero()
}
if opts.StringifyMapKeysWithFmt {
enc.StringifyMapKeysWithFmt()
}
Expand Down
Loading
Loading