Skip to content

Commit 4cb576d

Browse files
authored
GODRIVER-3081 Fix zero value detection for empty slices and maps. (#1546)
1 parent ec15401 commit 4cb576d

File tree

4 files changed

+100
-57
lines changed

4 files changed

+100
-57
lines changed

bson/bsoncodec/struct_codec.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,17 @@ func (sc *StructCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val
189189

190190
encoder := desc.encoder
191191

192-
var zero bool
192+
var empty bool
193193
if cz, ok := encoder.(CodecZeroer); ok {
194-
zero = cz.IsTypeZero(rv.Interface())
194+
empty = cz.IsTypeZero(rv.Interface())
195195
} else if rv.Kind() == reflect.Interface {
196-
// isZero will not treat an interface rv as an interface, so we need to check for the
197-
// zero interface separately.
198-
zero = rv.IsNil()
196+
// isEmpty will not treat an interface rv as an interface, so we need to check for the
197+
// nil interface separately.
198+
empty = rv.IsNil()
199199
} else {
200-
zero = isZero(rv, sc.EncodeOmitDefaultStruct || ec.omitZeroStruct)
200+
empty = isEmpty(rv, sc.EncodeOmitDefaultStruct || ec.omitZeroStruct)
201201
}
202-
if desc.omitEmpty && zero {
202+
if desc.omitEmpty && empty {
203203
continue
204204
}
205205

@@ -391,12 +391,15 @@ func (sc *StructCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val
391391
return nil
392392
}
393393

394-
func isZero(v reflect.Value, omitZeroStruct bool) bool {
394+
func isEmpty(v reflect.Value, omitZeroStruct bool) bool {
395395
kind := v.Kind()
396396
if (kind != reflect.Ptr || !v.IsNil()) && v.Type().Implements(tZeroer) {
397397
return v.Interface().(Zeroer).IsZero()
398398
}
399-
if kind == reflect.Struct {
399+
switch kind {
400+
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
401+
return v.Len() == 0
402+
case reflect.Struct:
400403
if !omitZeroStruct {
401404
return false
402405
}
@@ -410,7 +413,7 @@ func isZero(v reflect.Value, omitZeroStruct bool) bool {
410413
if ff.PkgPath != "" && !ff.Anonymous {
411414
continue // Private field
412415
}
413-
if !isZero(v.Field(i), omitZeroStruct) {
416+
if !isEmpty(v.Field(i), omitZeroStruct) {
414417
return false
415418
}
416419
}

bson/bsoncodec/struct_codec_test.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ func TestIsZero(t *testing.T) {
140140
omitZeroStruct: true,
141141
want: false,
142142
},
143+
{
144+
description: "empty map",
145+
value: map[string]string{},
146+
want: true,
147+
},
148+
{
149+
description: "empty slice",
150+
value: []struct{}{},
151+
want: true,
152+
},
153+
{
154+
description: "empty string",
155+
value: "",
156+
want: true,
157+
},
143158
}
144159

145160
for _, tc := range testCases {
@@ -148,8 +163,8 @@ func TestIsZero(t *testing.T) {
148163
t.Run(tc.description, func(t *testing.T) {
149164
t.Parallel()
150165

151-
got := isZero(reflect.ValueOf(tc.value), tc.omitZeroStruct)
152-
assert.Equal(t, tc.want, got, "expected and actual isZero return are different")
166+
got := isEmpty(reflect.ValueOf(tc.value), tc.omitZeroStruct)
167+
assert.Equal(t, tc.want, got, "expected and actual isEmpty return are different")
153168
})
154169
}
155170
}

bson/doc.go

+40-44
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
// Package bson is a library for reading, writing, and manipulating BSON. BSON is a binary serialization format used to
88
// store documents and make remote procedure calls in MongoDB. The BSON specification is located at https://bsonspec.org.
9-
// The BSON library handles marshalling and unmarshalling of values through a configurable codec system. For a description
10-
// of the codec system and examples of registering custom codecs, see the bsoncodec package. For additional information and
11-
// usage examples, check out the [Work with BSON] page in the Go Driver docs site.
9+
// The BSON library handles marshaling and unmarshaling of values through a configurable codec system. For a description
10+
// of the codec system and examples of registering custom codecs, see the bsoncodec package. For additional information
11+
// and usage examples, check out the [Work with BSON] page in the Go Driver docs site.
1212
//
1313
// # Raw BSON
1414
//
@@ -38,7 +38,7 @@
3838
// bson.D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}}
3939
// bson.M{"foo": "bar", "hello": "world", "pi": 3.14159}
4040
//
41-
// When decoding BSON to a D or M, the following type mappings apply when unmarshalling:
41+
// When decoding BSON to a D or M, the following type mappings apply when unmarshaling:
4242
//
4343
// 1. BSON int32 unmarshals to an int32.
4444
// 2. BSON int64 unmarshals to an int64.
@@ -62,7 +62,7 @@
6262
// 20. BSON DBPointer unmarshals to a primitive.DBPointer.
6363
// 21. BSON symbol unmarshals to a primitive.Symbol.
6464
//
65-
// The above mappings also apply when marshalling a D or M to BSON. Some other useful marshalling mappings are:
65+
// The above mappings also apply when marshaling a D or M to BSON. Some other useful marshaling mappings are:
6666
//
6767
// 1. time.Time marshals to a BSON datetime.
6868
// 2. int8, int16, and int32 marshal to a BSON int32.
@@ -71,73 +71,69 @@
7171
// 4. int64 marshals to BSON int64 (unless [Encoder.IntMinSize] is set).
7272
// 5. uint8 and uint16 marshal to a BSON int32.
7373
// 6. uint, uint32, and uint64 marshal to a BSON int64 (unless [Encoder.IntMinSize] is set).
74-
// 7. BSON null and undefined values will unmarshal into the zero value of a field (e.g. unmarshalling a BSON null or
74+
// 7. BSON null and undefined values will unmarshal into the zero value of a field (e.g. unmarshaling a BSON null or
7575
// undefined value into a string will yield the empty string.).
7676
//
7777
// # Structs
7878
//
79-
// Structs can be marshalled/unmarshalled to/from BSON or Extended JSON. When transforming structs to/from BSON or Extended
79+
// Structs can be marshaled/unmarshaled to/from BSON or Extended JSON. When transforming structs to/from BSON or Extended
8080
// JSON, the following rules apply:
8181
//
82-
// 1. Only exported fields in structs will be marshalled or unmarshalled.
82+
// 1. Only exported fields in structs will be marshaled or unmarshaled.
8383
//
84-
// 2. When marshalling a struct, each field will be lowercased to generate the key for the corresponding BSON element.
84+
// 2. When marshaling a struct, each field will be lowercased to generate the key for the corresponding BSON element.
8585
// For example, a struct field named "Foo" will generate key "foo". This can be overridden via a struct tag (e.g.
8686
// `bson:"fooField"` to generate key "fooField" instead).
8787
//
88-
// 3. An embedded struct field is marshalled as a subdocument. The key will be the lowercased name of the field's type.
88+
// 3. An embedded struct field is marshaled as a subdocument. The key will be the lowercased name of the field's type.
8989
//
90-
// 4. A pointer field is marshalled as the underlying type if the pointer is non-nil. If the pointer is nil, it is
91-
// marshalled as a BSON null value.
90+
// 4. A pointer field is marshaled as the underlying type if the pointer is non-nil. If the pointer is nil, it is
91+
// marshaled as a BSON null value.
9292
//
93-
// 5. When unmarshalling, a field of type interface{} will follow the D/M type mappings listed above. BSON documents
94-
// unmarshalled into an interface{} field will be unmarshalled as a D.
93+
// 5. When unmarshaling, a field of type interface{} will follow the D/M type mappings listed above. BSON documents
94+
// unmarshaled into an interface{} field will be unmarshaled as a D.
9595
//
9696
// The encoding of each struct field can be customized by the "bson" struct tag.
9797
//
9898
// This tag behavior is configurable, and different struct tag behavior can be configured by initializing a new
99-
// bsoncodec.StructCodec with the desired tag parser and registering that StructCodec onto the Registry. By default, JSON tags
100-
// are not honored, but that can be enabled by creating a StructCodec with JSONFallbackStructTagParser, like below:
99+
// bsoncodec.StructCodec with the desired tag parser and registering that StructCodec onto the Registry. By default, JSON
100+
// tags are not honored, but that can be enabled by creating a StructCodec with JSONFallbackStructTagParser, like below:
101101
//
102102
// Example:
103103
//
104104
// structcodec, _ := bsoncodec.NewStructCodec(bsoncodec.JSONFallbackStructTagParser)
105105
//
106106
// The bson tag gives the name of the field, possibly followed by a comma-separated list of options.
107-
// The name may be empty in order to specify options without overriding the default field name. The following options can be used
108-
// to configure behavior:
109-
//
110-
// 1. omitempty: If the omitempty struct tag is specified on a field, the field will not be marshalled if it is set to
111-
// the zero value. Fields with language primitive types such as integers, booleans, and strings are considered empty if
112-
// their value is equal to the zero value for the type (i.e. 0 for integers, false for booleans, and "" for strings).
113-
// Slices, maps, and arrays are considered empty if they are of length zero. Interfaces and pointers are considered
114-
// empty if their value is nil. By default, structs are only considered empty if the struct type implements the
115-
// bsoncodec.Zeroer interface and the IsZero method returns true. Struct fields whose types do not implement Zeroer are
116-
// never considered empty and will be marshalled as embedded documents.
107+
// The name may be empty in order to specify options without overriding the default field name. The following options can
108+
// be used to configure behavior:
109+
//
110+
// 1. omitempty: If the omitempty struct tag is specified on a field, the field will be omitted from the marshaling if
111+
// the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array,
112+
// slice, map, or string.
117113
// NOTE: It is recommended that this tag be used for all slice and map fields.
118114
//
119115
// 2. minsize: If the minsize struct tag is specified on a field of type int64, uint, uint32, or uint64 and the value of
120-
// the field can fit in a signed int32, the field will be serialized as a BSON int32 rather than a BSON int64. For other
121-
// types, this tag is ignored.
116+
// the field can fit in a signed int32, the field will be serialized as a BSON int32 rather than a BSON int64. For
117+
// other types, this tag is ignored.
122118
//
123-
// 3. truncate: If the truncate struct tag is specified on a field with a non-float numeric type, BSON doubles unmarshalled
124-
// into that field will be truncated at the decimal point. For example, if 3.14 is unmarshalled into a field of type int,
125-
// it will be unmarshalled as 3. If this tag is not specified, the decoder will throw an error if the value cannot be
126-
// decoded without losing precision. For float64 or non-numeric types, this tag is ignored.
119+
// 3. truncate: If the truncate struct tag is specified on a field with a non-float numeric type, BSON doubles
120+
// unmarshaled into that field will be truncated at the decimal point. For example, if 3.14 is unmarshaled into a
121+
// field of type int, it will be unmarshaled as 3. If this tag is not specified, the decoder will throw an error if
122+
// the value cannot be decoded without losing precision. For float64 or non-numeric types, this tag is ignored.
127123
//
128124
// 4. inline: If the inline struct tag is specified for a struct or map field, the field will be "flattened" when
129-
// marshalling and "un-flattened" when unmarshalling. This means that all of the fields in that struct/map will be
130-
// pulled up one level and will become top-level fields rather than being fields in a nested document. For example, if a
131-
// map field named "Map" with value map[string]interface{}{"foo": "bar"} is inlined, the resulting document will be
132-
// {"foo": "bar"} instead of {"map": {"foo": "bar"}}. There can only be one inlined map field in a struct. If there are
133-
// duplicated fields in the resulting document when an inlined struct is marshalled, the inlined field will be overwritten.
134-
// If there are duplicated fields in the resulting document when an inlined map is marshalled, an error will be returned.
135-
// This tag can be used with fields that are pointers to structs. If an inlined pointer field is nil, it will not be
136-
// marshalled. For fields that are not maps or structs, this tag is ignored.
137-
//
138-
// # Marshalling and Unmarshalling
139-
//
140-
// Manually marshalling and unmarshalling can be done with the Marshal and Unmarshal family of functions.
125+
// marshaling and "un-flattened" when unmarshaling. This means that all of the fields in that struct/map will be
126+
// pulled up one level and will become top-level fields rather than being fields in a nested document. For example,
127+
// if a map field named "Map" with value map[string]interface{}{"foo": "bar"} is inlined, the resulting document will
128+
// be {"foo": "bar"} instead of {"map": {"foo": "bar"}}. There can only be one inlined map field in a struct. If
129+
// there are duplicated fields in the resulting document when an inlined struct is marshaled, the inlined field will
130+
// be overwritten. If there are duplicated fields in the resulting document when an inlined map is marshaled, an
131+
// error will be returned. This tag can be used with fields that are pointers to structs. If an inlined pointer field
132+
// is nil, it will not be marshaled. For fields that are not maps or structs, this tag is ignored.
133+
//
134+
// # Marshaling and Unmarshaling
135+
//
136+
// Manually marshaling and unmarshaling can be done with the Marshal and Unmarshal family of functions.
141137
//
142138
// [Work with BSON]: https://www.mongodb.com/docs/drivers/go/current/fundamentals/bson/
143139
package bson

bson/primitive_codecs_test.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,36 @@ func TestDefaultValueEncoders(t *testing.T) {
260260
docToBytes(D{{"a", primitive.Symbol("foobarbaz")}}),
261261
nil,
262262
},
263+
{
264+
"omitempty map",
265+
struct {
266+
T map[string]string `bson:",omitempty"`
267+
}{
268+
T: map[string]string{},
269+
},
270+
docToBytes(D{}),
271+
nil,
272+
},
273+
{
274+
"omitempty slice",
275+
struct {
276+
T []struct{} `bson:",omitempty"`
277+
}{
278+
T: []struct{}{},
279+
},
280+
docToBytes(D{}),
281+
nil,
282+
},
283+
{
284+
"omitempty string",
285+
struct {
286+
T string `bson:",omitempty"`
287+
}{
288+
T: "",
289+
},
290+
docToBytes(D{}),
291+
nil,
292+
},
263293
{
264294
"struct{}",
265295
struct {
@@ -598,7 +628,6 @@ func TestDefaultValueDecoders(t *testing.T) {
598628
llvrw = rc.llvrw
599629
}
600630
llvrw.T = t
601-
// var got interface{}
602631
if rc.val == cansetreflectiontest { // We're doing a CanSet reflection test
603632
err := tc.vd.DecodeValue(dc, llvrw, reflect.Value{})
604633
if !compareErrors(err, rc.err) {

0 commit comments

Comments
 (0)