Skip to content

Commit ae95da5

Browse files
committed
GODRIVER-2765 Add an "omitzero" BSON struct tag.
1 parent 7825d6d commit ae95da5

13 files changed

+207
-21
lines changed

bson/bsoncodec.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type EncodeContext struct {
8888
nilSliceAsEmpty bool
8989
nilByteSliceAsEmpty bool
9090
omitZeroStruct bool
91+
omitZero bool
9192
useJSONStructTags bool
9293
}
9394

bson/default_value_decoders_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2805,6 +2805,26 @@ func TestDefaultValueDecoders(t *testing.T) {
28052805
buildDocument(bsoncore.AppendStringElement(nil, "a", "bar")),
28062806
nil,
28072807
},
2808+
{
2809+
"omitzero",
2810+
struct {
2811+
A [4]int `bson:",omitzero"`
2812+
}{
2813+
A: [4]int{},
2814+
},
2815+
[]byte{0x05, 0x00, 0x00, 0x00, 0x00},
2816+
nil,
2817+
},
2818+
{
2819+
"omitzero, empty time",
2820+
struct {
2821+
A time.Time `bson:",omitzero"`
2822+
}{
2823+
A: time.Time{},
2824+
},
2825+
[]byte{0x05, 0x00, 0x00, 0x00, 0x00},
2826+
nil,
2827+
},
28082828
{
28092829
"struct{}",
28102830
struct {

bson/default_value_encoders_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,24 @@ func TestDefaultValueEncoders(t *testing.T) {
12471247
[]byte{0x05, 0x00, 0x00, 0x00, 0x00},
12481248
nil,
12491249
},
1250+
{
1251+
"omitzero",
1252+
struct {
1253+
A [4]int `bson:",omitzero"`
1254+
}{},
1255+
[]byte{0x05, 0x00, 0x00, 0x00, 0x00},
1256+
nil,
1257+
},
1258+
{
1259+
"omitzero, empty time",
1260+
struct {
1261+
A time.Time `bson:",omitzero"`
1262+
}{
1263+
A: time.Time{},
1264+
},
1265+
[]byte{0x05, 0x00, 0x00, 0x00, 0x00},
1266+
nil,
1267+
},
12501268
{
12511269
"no private fields",
12521270
noPrivateFields{a: "should be empty"},

bson/encoder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ func (e *Encoder) OmitZeroStruct() {
116116
e.ec.omitZeroStruct = true
117117
}
118118

119+
// OmitZero causes the Encoder to omit zero values from the marshaled BSON.
120+
func (e *Encoder) OmitZero() {
121+
e.ec.omitZero = true
122+
}
123+
119124
// UseJSONStructTags causes the Encoder to fall back to using the "json" struct tag if a "bson"
120125
// struct tag is not specified.
121126
func (e *Encoder) UseJSONStructTags() {

bson/encoder_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,17 @@ func TestEncoderConfiguration(t *testing.T) {
248248
}{},
249249
want: bsoncore.NewDocumentBuilder().Build(),
250250
},
251+
// Test that OmitZero omits zero values from the marshaled document.
252+
{
253+
description: "OmitZero",
254+
configure: func(enc *Encoder) {
255+
enc.OmitZero()
256+
},
257+
input: struct {
258+
Values [4]int
259+
}{},
260+
want: bsoncore.NewDocumentBuilder().Build(),
261+
},
251262
// Test that UseJSONStructTags causes the Encoder to fall back to "json" struct tags if
252263
// "bson" struct tags are not available.
253264
{

bson/primitive_codecs_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,46 @@ func TestPrimitiveValueEncoders(t *testing.T) {
253253
docToBytes(D{}),
254254
nil,
255255
},
256+
{
257+
"omitzero map",
258+
struct {
259+
T map[string]string `bson:",omitzero"`
260+
}{
261+
T: map[string]string{},
262+
},
263+
docToBytes(D{}),
264+
nil,
265+
},
266+
{
267+
"omitzero slice",
268+
struct {
269+
T []struct{} `bson:",omitzero"`
270+
}{
271+
T: []struct{}{},
272+
},
273+
docToBytes(D{}),
274+
nil,
275+
},
276+
{
277+
"omitzero array",
278+
struct {
279+
T [4]int `bson:",omitzero"`
280+
}{
281+
T: [4]int{},
282+
},
283+
docToBytes(D{}),
284+
nil,
285+
},
286+
{
287+
"omitzero string",
288+
struct {
289+
T string `bson:",omitzero"`
290+
}{
291+
T: "",
292+
},
293+
docToBytes(D{}),
294+
nil,
295+
},
256296
{
257297
"struct{}",
258298
struct {
@@ -743,6 +783,26 @@ func TestPrimitiveValueDecoders(t *testing.T) {
743783
docToBytes(D{}),
744784
nil,
745785
},
786+
{
787+
"omitzero",
788+
struct {
789+
A [4]int `bson:",omitzero"`
790+
}{
791+
A: [4]int{},
792+
},
793+
docToBytes(D{}),
794+
nil,
795+
},
796+
{
797+
"omitzero, empty time",
798+
struct {
799+
A time.Time `bson:",omitzero"`
800+
}{
801+
A: time.Time{},
802+
},
803+
docToBytes(D{}),
804+
nil,
805+
},
746806
{
747807
"no private fields",
748808
noPrivateFields{a: "should be empty"},
@@ -817,6 +877,18 @@ func TestPrimitiveValueDecoders(t *testing.T) {
817877
docToBytes(D{{"a", "bar"}}),
818878
nil,
819879
},
880+
{
881+
"inline, omitzero",
882+
struct {
883+
A string
884+
Foo zeroTest `bson:"omitzero,inline"`
885+
}{
886+
A: "bar",
887+
Foo: zeroTest{true},
888+
},
889+
docToBytes(D{{"a", "bar"}}),
890+
nil,
891+
},
820892
{
821893
"JavaScript to D",
822894
D{{"a", JavaScript(`function() { var hello = "world"; }`)}},

bson/struct_codec.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (sc *structCodec) EncodeValue(ec EncodeContext, vw ValueWriter, val reflect
125125
}
126126

127127
if errors.Is(err, errInvalidValue) {
128-
if desc.omitEmpty {
128+
if desc.omitEmpty || desc.omitZero {
129129
continue
130130
}
131131
vw2, err := dw.WriteDocumentElement(desc.name)
@@ -145,6 +145,10 @@ func (sc *structCodec) EncodeValue(ec EncodeContext, vw ValueWriter, val reflect
145145

146146
encoder := desc.encoder
147147

148+
if isZero(rv) && (desc.omitZero || ec.omitZero) {
149+
continue
150+
}
151+
148152
var empty bool
149153
if rv.Kind() == reflect.Interface {
150154
// 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
171175
nilSliceAsEmpty: ec.nilSliceAsEmpty,
172176
nilByteSliceAsEmpty: ec.nilByteSliceAsEmpty,
173177
omitZeroStruct: ec.omitZeroStruct,
178+
omitZero: ec.omitZero,
174179
useJSONStructTags: ec.useJSONStructTags,
175180
}
176181
err = encoder.EncodeValue(ectx, vw2, rv)
@@ -361,6 +366,35 @@ func (sc *structCodec) DecodeValue(dc DecodeContext, vr ValueReader, val reflect
361366
return nil
362367
}
363368

369+
func isZero(v reflect.Value) bool {
370+
kind := v.Kind()
371+
if (kind != reflect.Ptr || !v.IsNil()) && v.Type().Implements(tZeroer) {
372+
return v.Interface().(Zeroer).IsZero()
373+
}
374+
switch kind {
375+
case reflect.Array:
376+
n := v.Len()
377+
for i := 0; i < n; i++ {
378+
if !isZero(v.Index(i)) {
379+
return false
380+
}
381+
}
382+
case reflect.Struct:
383+
vt := v.Type()
384+
if vt == tTime {
385+
return v.Interface().(time.Time).IsZero()
386+
}
387+
numField := vt.NumField()
388+
for i := 0; i < numField; i++ {
389+
if !isZero(v.Field(i)) {
390+
return false
391+
}
392+
}
393+
return true
394+
}
395+
return v.IsZero()
396+
}
397+
364398
func isEmpty(v reflect.Value, omitZeroStruct bool) bool {
365399
kind := v.Kind()
366400
if (kind != reflect.Ptr || !v.IsNil()) && v.Type().Implements(tZeroer) {
@@ -404,6 +438,7 @@ type fieldDescription struct {
404438
fieldName string // struct field name
405439
idx int
406440
omitEmpty bool
441+
omitZero bool
407442
minSize bool
408443
truncate bool
409444
inline []int
@@ -517,6 +552,7 @@ func (sc *structCodec) describeStructSlow(
517552
}
518553
description.name = stags.Name
519554
description.omitEmpty = stags.OmitEmpty
555+
description.omitZero = stags.OmitZero
520556
description.minSize = stags.MinSize
521557
description.truncate = stags.Truncate
522558

bson/struct_tag_parser.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
type structTags struct {
3939
Name string
4040
OmitEmpty bool
41+
OmitZero bool
4142
MinSize bool
4243
Truncate bool
4344
Inline bool
@@ -108,6 +109,8 @@ func parseTags(key string, tag string) (*structTags, error) {
108109
switch str {
109110
case "omitempty":
110111
st.OmitEmpty = true
112+
case "omitzero":
113+
st.OmitZero = true
111114
case "minsize":
112115
st.MinSize = true
113116
case "truncate":

bson/struct_tag_parser_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,26 @@ func TestStructTagParsers(t *testing.T) {
4646
},
4747
{
4848
"default all options",
49-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)},
50-
&structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
49+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,omitzero,minsize,truncate,inline`)},
50+
&structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
5151
parseStructTags,
5252
},
5353
{
5454
"default all options default name",
55-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)},
56-
&structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
55+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,omitzero,minsize,truncate,inline`)},
56+
&structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
5757
parseStructTags,
5858
},
5959
{
6060
"default bson tag all options",
61-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)},
62-
&structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
61+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,omitzero,minsize,truncate,inline"`)},
62+
&structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
6363
parseStructTags,
6464
},
6565
{
6666
"default bson tag all options default name",
67-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)},
68-
&structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
67+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,omitzero,minsize,truncate,inline"`)},
68+
&structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
6969
parseStructTags,
7070
},
7171
{
@@ -100,38 +100,38 @@ func TestStructTagParsers(t *testing.T) {
100100
},
101101
{
102102
"JSONFallback all options",
103-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,minsize,truncate,inline`)},
104-
&structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
103+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bar,omitempty,omitzero,minsize,truncate,inline`)},
104+
&structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
105105
parseJSONStructTags,
106106
},
107107
{
108108
"JSONFallback all options default name",
109-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,minsize,truncate,inline`)},
110-
&structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
109+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`,omitempty,omitzero,minsize,truncate,inline`)},
110+
&structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
111111
parseJSONStructTags,
112112
},
113113
{
114114
"JSONFallback bson tag all options",
115-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,minsize,truncate,inline"`)},
116-
&structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
115+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:"bar,omitempty,omitzero,minsize,truncate,inline"`)},
116+
&structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
117117
parseJSONStructTags,
118118
},
119119
{
120120
"JSONFallback bson tag all options default name",
121-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,minsize,truncate,inline"`)},
122-
&structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
121+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`bson:",omitempty,omitzero,minsize,truncate,inline"`)},
122+
&structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
123123
parseJSONStructTags,
124124
},
125125
{
126126
"JSONFallback json tag all options",
127-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:"bar,omitempty,minsize,truncate,inline"`)},
128-
&structTags{Name: "bar", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
127+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:"bar,omitempty,omitzero,minsize,truncate,inline"`)},
128+
&structTags{Name: "bar", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
129129
parseJSONStructTags,
130130
},
131131
{
132132
"JSONFallback json tag all options default name",
133-
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:",omitempty,minsize,truncate,inline"`)},
134-
&structTags{Name: "foo", OmitEmpty: true, MinSize: true, Truncate: true, Inline: true},
133+
reflect.StructField{Name: "foo", Tag: reflect.StructTag(`json:",omitempty,omitzero,minsize,truncate,inline"`)},
134+
&structTags{Name: "foo", OmitEmpty: true, OmitZero: true, MinSize: true, Truncate: true, Inline: true},
135135
parseJSONStructTags,
136136
},
137137
{

internal/integration/client_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,18 @@ func TestClient_BSONOptions(t *testing.T) {
956956
want: &bson.D{},
957957
wantRaw: bson.Raw(bsoncore.NewDocumentBuilder().Build()),
958958
},
959+
{
960+
name: "OmitZero",
961+
bsonOpts: &options.BSONOptions{
962+
OmitZero: true,
963+
},
964+
doc: struct {
965+
X [4]int
966+
}{},
967+
decodeInto: func() interface{} { return &bson.D{} },
968+
want: &bson.D{},
969+
wantRaw: bson.Raw(bsoncore.NewDocumentBuilder().Build()),
970+
},
959971
{
960972
name: "StringifyMapKeysWithFmt",
961973
bsonOpts: &options.BSONOptions{

0 commit comments

Comments
 (0)