Skip to content

Commit e005697

Browse files
callthingsoffbradfitz
authored andcommitted
encoding/json: add omitzero option
Fixes golang#45669 Change-Id: Ic13523c0b3acdfc5b3e29a717bc62fde302ed8fd GitHub-Last-Rev: 57030f2 GitHub-Pull-Request: golang#69622 Reviewed-on: https://go-review.googlesource.com/c/go/+/615676 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Joseph Tsai <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 06a4e4f commit e005697

File tree

3 files changed

+248
-4
lines changed

3 files changed

+248
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
When marshaling, a struct field with the new `omitzero` option in the struct field
2+
tag will be omitted if its value is zero. If the field type has an `IsZero() bool`
3+
method, that will be used to determine whether the value is zero. Otherwise, the
4+
value is zero if it is [the zero value for its type](/ref/spec#The_zero_value).
5+
6+
If both `omitempty` and `omitzero` are specified, the field will be omitted if the
7+
value is either empty or zero (or both).

src/encoding/json/encode.go

+56-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ import (
9999
// // Field appears in JSON as key "-".
100100
// Field int `json:"-,"`
101101
//
102+
// The "omitzero" option specifies that the field should be omitted
103+
// from the encoding if the field has a zero value, according to rules:
104+
//
105+
// 1) If the field type has an "IsZero() bool" method, that will be used to
106+
// determine whether the value is zero.
107+
//
108+
// 2) Otherwise, the value is zero if it is the zero value for its type.
109+
//
110+
// If both "omitempty" and "omitzero" are specified, the field will be omitted
111+
// if the value is either empty or zero (or both).
112+
//
102113
// The "string" option signals that a field is stored as JSON inside a
103114
// JSON-encoded string. It applies only to fields of string, floating point,
104115
// integer, or boolean types. This extra level of encoding is sometimes used
@@ -701,7 +712,8 @@ FieldLoop:
701712
fv = fv.Field(i)
702713
}
703714

704-
if f.omitEmpty && isEmptyValue(fv) {
715+
if (f.omitEmpty && isEmptyValue(fv)) ||
716+
(f.omitZero && (f.isZero == nil && fv.IsZero() || (f.isZero != nil && f.isZero(fv)))) {
705717
continue
706718
}
707719
e.WriteByte(next)
@@ -1048,11 +1060,19 @@ type field struct {
10481060
index []int
10491061
typ reflect.Type
10501062
omitEmpty bool
1063+
omitZero bool
1064+
isZero func(reflect.Value) bool
10511065
quoted bool
10521066

10531067
encoder encoderFunc
10541068
}
10551069

1070+
type isZeroer interface {
1071+
IsZero() bool
1072+
}
1073+
1074+
var isZeroerType = reflect.TypeFor[isZeroer]()
1075+
10561076
// typeFields returns a list of fields that JSON should recognize for the given type.
10571077
// The algorithm is breadth-first search over the set of structs to include - the top struct
10581078
// and then any reachable anonymous structs.
@@ -1154,6 +1174,7 @@ func typeFields(t reflect.Type) structFields {
11541174
index: index,
11551175
typ: ft,
11561176
omitEmpty: opts.Contains("omitempty"),
1177+
omitZero: opts.Contains("omitzero"),
11571178
quoted: quoted,
11581179
}
11591180
field.nameBytes = []byte(field.name)
@@ -1163,6 +1184,40 @@ func typeFields(t reflect.Type) structFields {
11631184
field.nameEscHTML = `"` + string(nameEscBuf) + `":`
11641185
field.nameNonEsc = `"` + field.name + `":`
11651186

1187+
if field.omitZero {
1188+
t := sf.Type
1189+
// Provide a function that uses a type's IsZero method.
1190+
switch {
1191+
case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
1192+
field.isZero = func(v reflect.Value) bool {
1193+
// Avoid panics calling IsZero on a nil interface or
1194+
// non-nil interface with nil pointer.
1195+
return v.IsNil() ||
1196+
(v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) ||
1197+
v.Interface().(isZeroer).IsZero()
1198+
}
1199+
case t.Kind() == reflect.Pointer && t.Implements(isZeroerType):
1200+
field.isZero = func(v reflect.Value) bool {
1201+
// Avoid panics calling IsZero on nil pointer.
1202+
return v.IsNil() || v.Interface().(isZeroer).IsZero()
1203+
}
1204+
case t.Implements(isZeroerType):
1205+
field.isZero = func(v reflect.Value) bool {
1206+
return v.Interface().(isZeroer).IsZero()
1207+
}
1208+
case reflect.PointerTo(t).Implements(isZeroerType):
1209+
field.isZero = func(v reflect.Value) bool {
1210+
if !v.CanAddr() {
1211+
// Temporarily box v so we can take the address.
1212+
v2 := reflect.New(v.Type()).Elem()
1213+
v2.Set(v)
1214+
v = v2
1215+
}
1216+
return v.Addr().Interface().(isZeroer).IsZero()
1217+
}
1218+
}
1219+
}
1220+
11661221
fields = append(fields, field)
11671222
if count[f.typ] > 1 {
11681223
// If there were multiple instances, add a second,

src/encoding/json/encode_test.go

+185-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import (
1515
"runtime/debug"
1616
"strconv"
1717
"testing"
18+
"time"
1819
)
1920

20-
type Optionals struct {
21+
type OptionalsEmpty struct {
2122
Sr string `json:"sr"`
2223
So string `json:"so,omitempty"`
2324
Sw string `json:"-"`
@@ -45,7 +46,7 @@ type Optionals struct {
4546
}
4647

4748
func TestOmitEmpty(t *testing.T) {
48-
var want = `{
49+
const want = `{
4950
"sr": "",
5051
"omitempty": 0,
5152
"slr": null,
@@ -56,7 +57,7 @@ func TestOmitEmpty(t *testing.T) {
5657
"str": {},
5758
"sto": {}
5859
}`
59-
var o Optionals
60+
var o OptionalsEmpty
6061
o.Sw = "something"
6162
o.Mr = map[string]any{}
6263
o.Mo = map[string]any{}
@@ -70,6 +71,187 @@ func TestOmitEmpty(t *testing.T) {
7071
}
7172
}
7273

74+
type NonZeroStruct struct{}
75+
76+
func (nzs NonZeroStruct) IsZero() bool {
77+
return false
78+
}
79+
80+
type NoPanicStruct struct {
81+
Int int `json:"int,omitzero"`
82+
}
83+
84+
func (nps *NoPanicStruct) IsZero() bool {
85+
return nps.Int != 0
86+
}
87+
88+
type OptionalsZero struct {
89+
Sr string `json:"sr"`
90+
So string `json:"so,omitzero"`
91+
Sw string `json:"-"`
92+
93+
Ir int `json:"omitzero"` // actually named omitzero, not an option
94+
Io int `json:"io,omitzero"`
95+
96+
Slr []string `json:"slr,random"`
97+
Slo []string `json:"slo,omitzero"`
98+
SloNonNil []string `json:"slononnil,omitzero"`
99+
100+
Mr map[string]any `json:"mr"`
101+
Mo map[string]any `json:",omitzero"`
102+
Moo map[string]any `json:"moo,omitzero"`
103+
104+
Fr float64 `json:"fr"`
105+
Fo float64 `json:"fo,omitzero"`
106+
Foo float64 `json:"foo,omitzero"`
107+
Foo2 [2]float64 `json:"foo2,omitzero"`
108+
109+
Br bool `json:"br"`
110+
Bo bool `json:"bo,omitzero"`
111+
112+
Ur uint `json:"ur"`
113+
Uo uint `json:"uo,omitzero"`
114+
115+
Str struct{} `json:"str"`
116+
Sto struct{} `json:"sto,omitzero"`
117+
118+
Time time.Time `json:"time,omitzero"`
119+
TimeLocal time.Time `json:"timelocal,omitzero"`
120+
Nzs NonZeroStruct `json:"nzs,omitzero"`
121+
122+
NilIsZeroer isZeroer `json:"niliszeroer,omitzero"` // nil interface
123+
NonNilIsZeroer isZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface
124+
NoPanicStruct0 isZeroer `json:"nps0,omitzero"` // non-nil interface with nil pointer
125+
NoPanicStruct1 isZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer
126+
NoPanicStruct2 *NoPanicStruct `json:"nps2,omitzero"` // nil pointer
127+
NoPanicStruct3 *NoPanicStruct `json:"nps3,omitzero"` // non-nil pointer
128+
NoPanicStruct4 NoPanicStruct `json:"nps4,omitzero"` // concrete type
129+
}
130+
131+
func TestOmitZero(t *testing.T) {
132+
const want = `{
133+
"sr": "",
134+
"omitzero": 0,
135+
"slr": null,
136+
"slononnil": [],
137+
"mr": {},
138+
"Mo": {},
139+
"fr": 0,
140+
"br": false,
141+
"ur": 0,
142+
"str": {},
143+
"nzs": {},
144+
"nps1": {},
145+
"nps3": {},
146+
"nps4": {}
147+
}`
148+
var o OptionalsZero
149+
o.Sw = "something"
150+
o.SloNonNil = make([]string, 0)
151+
o.Mr = map[string]any{}
152+
o.Mo = map[string]any{}
153+
154+
o.Foo = -0
155+
o.Foo2 = [2]float64{+0, -0}
156+
157+
o.TimeLocal = time.Time{}.Local()
158+
159+
o.NonNilIsZeroer = time.Time{}
160+
o.NoPanicStruct0 = (*NoPanicStruct)(nil)
161+
o.NoPanicStruct1 = &NoPanicStruct{}
162+
o.NoPanicStruct3 = &NoPanicStruct{}
163+
164+
got, err := MarshalIndent(&o, "", " ")
165+
if err != nil {
166+
t.Fatalf("MarshalIndent error: %v", err)
167+
}
168+
if got := string(got); got != want {
169+
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", indentNewlines(got), indentNewlines(want))
170+
}
171+
}
172+
173+
func TestOmitZeroMap(t *testing.T) {
174+
const want = `{
175+
"foo": {
176+
"sr": "",
177+
"omitzero": 0,
178+
"slr": null,
179+
"mr": null,
180+
"fr": 0,
181+
"br": false,
182+
"ur": 0,
183+
"str": {},
184+
"nzs": {},
185+
"nps4": {}
186+
}
187+
}`
188+
m := map[string]OptionalsZero{"foo": {}}
189+
got, err := MarshalIndent(m, "", " ")
190+
if err != nil {
191+
t.Fatalf("MarshalIndent error: %v", err)
192+
}
193+
if got := string(got); got != want {
194+
fmt.Println(got)
195+
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", indentNewlines(got), indentNewlines(want))
196+
}
197+
}
198+
199+
type OptionalsEmptyZero struct {
200+
Sr string `json:"sr"`
201+
So string `json:"so,omitempty,omitzero"`
202+
Sw string `json:"-"`
203+
204+
Io int `json:"io,omitempty,omitzero"`
205+
206+
Slr []string `json:"slr,random"`
207+
Slo []string `json:"slo,omitempty,omitzero"`
208+
SloNonNil []string `json:"slononnil,omitempty,omitzero"`
209+
210+
Mr map[string]any `json:"mr"`
211+
Mo map[string]any `json:",omitempty,omitzero"`
212+
213+
Fr float64 `json:"fr"`
214+
Fo float64 `json:"fo,omitempty,omitzero"`
215+
216+
Br bool `json:"br"`
217+
Bo bool `json:"bo,omitempty,omitzero"`
218+
219+
Ur uint `json:"ur"`
220+
Uo uint `json:"uo,omitempty,omitzero"`
221+
222+
Str struct{} `json:"str"`
223+
Sto struct{} `json:"sto,omitempty,omitzero"`
224+
225+
Time time.Time `json:"time,omitempty,omitzero"`
226+
Nzs NonZeroStruct `json:"nzs,omitempty,omitzero"`
227+
}
228+
229+
func TestOmitEmptyZero(t *testing.T) {
230+
const want = `{
231+
"sr": "",
232+
"slr": null,
233+
"mr": {},
234+
"fr": 0,
235+
"br": false,
236+
"ur": 0,
237+
"str": {},
238+
"nzs": {}
239+
}`
240+
var o OptionalsEmptyZero
241+
o.Sw = "something"
242+
o.SloNonNil = make([]string, 0)
243+
o.Mr = map[string]any{}
244+
o.Mo = map[string]any{}
245+
246+
got, err := MarshalIndent(&o, "", " ")
247+
if err != nil {
248+
t.Fatalf("MarshalIndent error: %v", err)
249+
}
250+
if got := string(got); got != want {
251+
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", indentNewlines(got), indentNewlines(want))
252+
}
253+
}
254+
73255
type StringTag struct {
74256
BoolStr bool `json:",string"`
75257
IntStr int64 `json:",string"`

0 commit comments

Comments
 (0)