Skip to content

Commit dda5e3a

Browse files
authored
Merge pull request kubernetes-sigs#286 from liggitt/omitzero
Add omitzero support
2 parents 9a51599 + 0e97094 commit dda5e3a

File tree

3 files changed

+171
-9
lines changed

3 files changed

+171
-9
lines changed

Diff for: value/jsontagutil.go

+59-4
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,77 @@ import (
2222
"strings"
2323
)
2424

25+
type isZeroer interface {
26+
IsZero() bool
27+
}
28+
29+
var isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
30+
31+
func reflectIsZero(dv reflect.Value) bool {
32+
return dv.IsZero()
33+
}
34+
35+
// OmitZeroFunc returns a function for a type for a given struct field
36+
// which determines if the value for that field is a zero value, matching
37+
// how the stdlib JSON implementation.
38+
func OmitZeroFunc(t reflect.Type) func(reflect.Value) bool {
39+
// Provide a function that uses a type's IsZero method.
40+
// This matches the go 1.24 custom IsZero() implementation matching
41+
switch {
42+
case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
43+
return func(v reflect.Value) bool {
44+
// Avoid panics calling IsZero on a nil interface or
45+
// non-nil interface with nil pointer.
46+
return safeIsNil(v) ||
47+
(v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) ||
48+
v.Interface().(isZeroer).IsZero()
49+
}
50+
case t.Kind() == reflect.Pointer && t.Implements(isZeroerType):
51+
return func(v reflect.Value) bool {
52+
// Avoid panics calling IsZero on nil pointer.
53+
return safeIsNil(v) || v.Interface().(isZeroer).IsZero()
54+
}
55+
case t.Implements(isZeroerType):
56+
return func(v reflect.Value) bool {
57+
return v.Interface().(isZeroer).IsZero()
58+
}
59+
case reflect.PointerTo(t).Implements(isZeroerType):
60+
return func(v reflect.Value) bool {
61+
if !v.CanAddr() {
62+
// Temporarily box v so we can take the address.
63+
v2 := reflect.New(v.Type()).Elem()
64+
v2.Set(v)
65+
v = v2
66+
}
67+
return v.Addr().Interface().(isZeroer).IsZero()
68+
}
69+
default:
70+
// default to the reflect.IsZero implementation
71+
return reflectIsZero
72+
}
73+
}
74+
2575
// TODO: This implements the same functionality as https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go#L236
2676
// but is based on the highly efficient approach from https://golang.org/src/encoding/json/encode.go
2777

28-
func lookupJsonTags(f reflect.StructField) (name string, omit bool, inline bool, omitempty bool) {
78+
func lookupJsonTags(f reflect.StructField) (name string, omit bool, inline bool, omitempty bool, omitzero func(reflect.Value) bool) {
2979
tag := f.Tag.Get("json")
3080
if tag == "-" {
31-
return "", true, false, false
81+
return "", true, false, false, nil
3282
}
3383
name, opts := parseTag(tag)
3484
if name == "" {
3585
name = f.Name
3686
}
37-
return name, false, opts.Contains("inline"), opts.Contains("omitempty")
87+
88+
if opts.Contains("omitzero") {
89+
omitzero = OmitZeroFunc(f.Type)
90+
}
91+
92+
return name, false, opts.Contains("inline"), opts.Contains("omitempty"), omitzero
3893
}
3994

40-
func isZero(v reflect.Value) bool {
95+
func isEmpty(v reflect.Value) bool {
4196
switch v.Kind() {
4297
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
4398
return v.Len() == 0

Diff for: value/reflectcache.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ type FieldCacheEntry struct {
5959
JsonName string
6060
// isOmitEmpty is true if the field has the json 'omitempty' tag.
6161
isOmitEmpty bool
62+
// omitzero is set if the field has the json 'omitzero' tag.
63+
omitzero func(reflect.Value) bool
6264
// fieldPath is a list of field indices (see FieldByIndex) to lookup the value of
6365
// a field in a reflect.Value struct. The field indices in the list form a path used
6466
// to traverse through intermediary 'inline' fields.
@@ -69,7 +71,13 @@ type FieldCacheEntry struct {
6971
}
7072

7173
func (f *FieldCacheEntry) CanOmit(fieldVal reflect.Value) bool {
72-
return f.isOmitEmpty && (safeIsNil(fieldVal) || isZero(fieldVal))
74+
if f.isOmitEmpty && (safeIsNil(fieldVal) || isEmpty(fieldVal)) {
75+
return true
76+
}
77+
if f.omitzero != nil && f.omitzero(fieldVal) {
78+
return true
79+
}
80+
return false
7381
}
7482

7583
// GetFrom returns the field identified by this FieldCacheEntry from the provided struct.
@@ -147,7 +155,7 @@ func typeReflectEntryOf(cm reflectCacheMap, t reflect.Type, updates reflectCache
147155
func buildStructCacheEntry(t reflect.Type, infos map[string]*FieldCacheEntry, fieldPath [][]int) {
148156
for i := 0; i < t.NumField(); i++ {
149157
field := t.Field(i)
150-
jsonName, omit, isInline, isOmitempty := lookupJsonTags(field)
158+
jsonName, omit, isInline, isOmitempty, omitzero := lookupJsonTags(field)
151159
if omit {
152160
continue
153161
}
@@ -161,7 +169,7 @@ func buildStructCacheEntry(t reflect.Type, infos map[string]*FieldCacheEntry, fi
161169
}
162170
continue
163171
}
164-
info := &FieldCacheEntry{JsonName: jsonName, isOmitEmpty: isOmitempty, fieldPath: append(fieldPath, field.Index), fieldType: field.Type}
172+
info := &FieldCacheEntry{JsonName: jsonName, isOmitEmpty: isOmitempty, omitzero: omitzero, fieldPath: append(fieldPath, field.Index), fieldType: field.Type}
165173
infos[jsonName] = info
166174
}
167175
}

Diff for: value/reflectcache_test.go

+101-2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func TestTimeToUnstructured(t *testing.T) {
144144

145145
func TestTypeReflectEntryOf(t *testing.T) {
146146
testString := ""
147+
testCustomType := customOmitZeroType{}
147148
tests := map[string]struct {
148149
arg interface{}
149150
want *TypeReflectCacheEntry
@@ -196,6 +197,62 @@ func TestTypeReflectEntryOf(t *testing.T) {
196197
},
197198
},
198199
},
200+
"StructWith*StringFieldOmitzero": {
201+
arg: struct {
202+
F1 *string `json:"f1,omitzero"`
203+
}{},
204+
want: &TypeReflectCacheEntry{
205+
structFields: map[string]*FieldCacheEntry{
206+
"f1": {
207+
JsonName: "f1",
208+
omitzero: func(v reflect.Value) bool { return v.IsZero() },
209+
fieldPath: [][]int{{0}},
210+
fieldType: reflect.TypeOf(&testString),
211+
TypeEntry: &TypeReflectCacheEntry{},
212+
},
213+
},
214+
orderedStructFields: []*FieldCacheEntry{
215+
{
216+
JsonName: "f1",
217+
omitzero: func(v reflect.Value) bool { return v.IsZero() },
218+
fieldPath: [][]int{{0}},
219+
fieldType: reflect.TypeOf(&testString),
220+
TypeEntry: &TypeReflectCacheEntry{},
221+
},
222+
},
223+
},
224+
},
225+
"StructWith*CustomFieldOmitzero": {
226+
arg: struct {
227+
F1 customOmitZeroType `json:"f1,omitzero"`
228+
}{},
229+
want: &TypeReflectCacheEntry{
230+
structFields: map[string]*FieldCacheEntry{
231+
"f1": {
232+
JsonName: "f1",
233+
omitzero: func(v reflect.Value) bool { return false },
234+
fieldPath: [][]int{{0}},
235+
fieldType: reflect.TypeOf(testCustomType),
236+
TypeEntry: &TypeReflectCacheEntry{
237+
structFields: map[string]*FieldCacheEntry{},
238+
orderedStructFields: []*FieldCacheEntry{},
239+
},
240+
},
241+
},
242+
orderedStructFields: []*FieldCacheEntry{
243+
{
244+
JsonName: "f1",
245+
omitzero: func(v reflect.Value) bool { return false },
246+
fieldPath: [][]int{{0}},
247+
fieldType: reflect.TypeOf(testCustomType),
248+
TypeEntry: &TypeReflectCacheEntry{
249+
structFields: map[string]*FieldCacheEntry{},
250+
orderedStructFields: []*FieldCacheEntry{},
251+
},
252+
},
253+
},
254+
},
255+
},
199256
"StructWithInlinedField": {
200257
arg: struct {
201258
F1 string `json:",inline"`
@@ -208,13 +265,55 @@ func TestTypeReflectEntryOf(t *testing.T) {
208265
}
209266
for name, tt := range tests {
210267
t.Run(name, func(t *testing.T) {
211-
if got := TypeReflectEntryOf(reflect.TypeOf(tt.arg)); !reflect.DeepEqual(got, tt.want) {
212-
t.Errorf("TypeReflectEntryOf() = %v, want %v", got, tt.want)
268+
got := TypeReflectEntryOf(reflect.TypeOf(tt.arg))
269+
270+
// evaluate non-comparable omitzero functions
271+
for k, v := range got.structFields {
272+
compareOmitZero(t, v.fieldType, v.omitzero, tt.want.structFields[k].omitzero)
273+
}
274+
for i, v := range got.orderedStructFields {
275+
compareOmitZero(t, v.fieldType, v.omitzero, tt.want.orderedStructFields[i].omitzero)
276+
}
277+
278+
// clear non-comparable omitzero functions
279+
for k, v := range got.structFields {
280+
v.omitzero = nil
281+
tt.want.structFields[k].omitzero = nil
282+
}
283+
for i, v := range got.orderedStructFields {
284+
v.omitzero = nil
285+
tt.want.orderedStructFields[i].omitzero = nil
286+
}
287+
288+
// compare remaining fields
289+
if !reflect.DeepEqual(got, tt.want) {
290+
t.Errorf("TypeReflectEntryOf() got\n%#v\nwant\n%#v", got, tt.want)
213291
}
214292
})
215293
}
216294
}
217295

296+
type customOmitZeroType struct {
297+
}
298+
299+
func (c *customOmitZeroType) IsZero() bool {
300+
return false
301+
}
302+
303+
func compareOmitZero(t *testing.T, fieldType reflect.Type, got, want func(reflect.Value) bool) {
304+
t.Helper()
305+
if (want == nil) != (got == nil) {
306+
t.Fatalf("wanted omitzero=%v, got omitzero=%v", (want == nil), (got == nil))
307+
}
308+
if want == nil {
309+
return
310+
}
311+
v := reflect.New(fieldType).Elem()
312+
if e, a := want(v), got(v); e != a {
313+
t.Fatalf("wanted omitzero()=%v, got omitzero()=%v", e, a)
314+
}
315+
}
316+
218317
func TestUnmarshal(t *testing.T) {
219318
for _, tc := range []struct {
220319
JSON string

0 commit comments

Comments
 (0)