Skip to content

Commit 2c9e0bd

Browse files
authored
Implement support for OmitZeroStructFields (#61)
By default, Marshal serializes Go structs with all fields even if each field is the zero Go value. This can lead to verbose output emitting fields for which it is often indistinguishable from the zero value of the field itself. The caller-specified OmitZeroStructFields is equivalent to specifying the omitzero tag option on every Go struct field. Static analysis of Go code used with JSON indicates that about 50% of all struct fields have `omitempty` specified. While `omitempty` is not the same as `omitzero`, it is probably safer to provide a caller-scoped OmitZeroStructFields option since doing so is more likely to produce output that is semantically equivalent to if the option was not specified at all. The exceptions to the above is if the field value contains a -0.0 or if it implements IsZero that treats certain values other than the zero Go value as "zero".
1 parent e27260c commit 2c9e0bd

File tree

4 files changed

+63
-1
lines changed

4 files changed

+63
-1
lines changed

arshal_default.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,8 @@ func makeStructArshaler(t reflect.Type) *arshaler {
945945

946946
// OmitZero skips the field if the Go value is zero,
947947
// which we can determine up front without calling the marshaler.
948-
if f.omitzero && ((f.isZero == nil && v.IsZero()) || (f.isZero != nil && f.isZero(v))) {
948+
if (f.omitzero || mo.Flags.Get(jsonflags.OmitZeroStructFields)) &&
949+
((f.isZero == nil && v.IsZero()) || (f.isZero != nil && f.isZero(v))) {
949950
continue
950951
}
951952

arshal_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,11 @@ func TestMarshal(t *testing.T) {
13961396
name: jsontest.Name("Structs/OmitZero/Zero"),
13971397
in: structOmitZeroAll{},
13981398
want: `{}`,
1399+
}, {
1400+
name: jsontest.Name("Structs/OmitZeroOption/Zero"),
1401+
opts: []Options{OmitZeroStructFields(true)},
1402+
in: structAll{},
1403+
want: `{}`,
13991404
}, {
14001405
name: jsontest.Name("Structs/OmitZero/NonZero"),
14011406
opts: []Options{jsontext.Multiline(true)},
@@ -1453,6 +1458,45 @@ func TestMarshal(t *testing.T) {
14531458
],
14541459
"Pointer": {},
14551460
"Interface": null
1461+
}`,
1462+
}, {
1463+
name: jsontest.Name("Structs/OmitZeroOption/NonZero"),
1464+
opts: []Options{OmitZeroStructFields(true), jsontext.Multiline(true)},
1465+
in: structAll{
1466+
Bool: true,
1467+
String: " ",
1468+
Bytes: []byte{},
1469+
Int: 1,
1470+
Uint: 1,
1471+
Float: math.SmallestNonzeroFloat64,
1472+
Map: map[string]string{},
1473+
StructScalars: structScalars{unexported: true},
1474+
StructSlices: structSlices{Ignored: true},
1475+
StructMaps: structMaps{MapBool: map[string]bool{}},
1476+
Slice: []string{},
1477+
Array: [1]string{" "},
1478+
Pointer: new(structAll),
1479+
Interface: (*structAll)(nil),
1480+
},
1481+
want: `{
1482+
"Bool": true,
1483+
"String": " ",
1484+
"Bytes": "",
1485+
"Int": 1,
1486+
"Uint": 1,
1487+
"Float": 5e-324,
1488+
"Map": {},
1489+
"StructScalars": {},
1490+
"StructMaps": {
1491+
"MapBool": {}
1492+
},
1493+
"StructSlices": {},
1494+
"Slice": [],
1495+
"Array": [
1496+
" "
1497+
],
1498+
"Pointer": {},
1499+
"Interface": null
14561500
}`,
14571501
}, {
14581502
name: jsontest.Name("Structs/OmitZeroMethod/Zero"),

internal/jsonflags/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const (
106106
Deterministic // marshal only
107107
FormatNilMapAsNull // marshal only
108108
FormatNilSliceAsNull // marshal only
109+
OmitZeroStructFields // marshal only
109110
MatchCaseInsensitiveNames // marshal or unmarshal
110111
DiscardUnknownMembers // marshal only
111112
RejectUnknownMembers // unmarshal only

options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ func FormatNilMapAsNull(v bool) Options {
167167
}
168168
}
169169

170+
// OmitZeroStructFields specifies that a Go struct should marshal in such a way
171+
// that all struct fields that are zero are omitted from the marshaled output
172+
// if the value is zero as determined by the "IsZero() bool" method if present,
173+
// otherwise based on whether the field is the zero Go value.
174+
// This is semantically equivalent to specifying the `omitzero` tag option
175+
// on every field in a Go struct.
176+
//
177+
// This only affects marshaling and is ignored when unmarshaling.
178+
func OmitZeroStructFields(v bool) Options {
179+
if v {
180+
return jsonflags.OmitZeroStructFields | 1
181+
} else {
182+
return jsonflags.OmitZeroStructFields | 0
183+
}
184+
}
185+
170186
// MatchCaseInsensitiveNames specifies that JSON object members are matched
171187
// against Go struct fields using a case-insensitive match of the name.
172188
// Go struct fields explicitly marked with `strictcase` or `nocase`

0 commit comments

Comments
 (0)