From aae92fa2ea616b13aa41c18f6f09e9104a7c02e2 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Wed, 25 Sep 2024 19:48:52 +0800 Subject: [PATCH] encoding/json: add omitzero option Fixes #45669 Change-Id: Idec483a03968cc671c8da27804589008b10864a1 --- .../6-stdlib/99-minor/encoding/json/45669.md | 7 + src/encoding/json/encode.go | 40 +++++- src/encoding/json/encode_test.go | 127 +++++++++++++++++- 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 doc/next/6-stdlib/99-minor/encoding/json/45669.md diff --git a/doc/next/6-stdlib/99-minor/encoding/json/45669.md b/doc/next/6-stdlib/99-minor/encoding/json/45669.md new file mode 100644 index 00000000000000..1d62ddc968222f --- /dev/null +++ b/doc/next/6-stdlib/99-minor/encoding/json/45669.md @@ -0,0 +1,7 @@ +When marshaling, a struct field with the new `omitzero` option in the struct field +tag will be omitted if its value is zero. If the field type has an `IsZero() bool` +method, that will be used to determine whether the value is zero. Otherwise, the +value is zero if it is [the zero value for its type](/ref/spec#The_zero_value). + +If both `omitempty` and `omitzero` are specified, the field will be omitted if the +value is either empty or zero (or both). diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 988de716124862..e31746fbef3fc3 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -99,6 +99,32 @@ import ( // // Field appears in JSON as key "-". // Field int `json:"-,"` // +// The "omitzero" option specifies that the field should be omitted +// from the encoding if the field has a zero value, according to: +// +// 1) If the field type has an "IsZero() bool" method, that will be used to +// determine whether the value is zero. +// +// 2) Otherwise, the value is zero if it is the zero value for its type. +// +// Examples of struct field tags and their meanings: +// +// // Field appears in JSON as key "myName". +// Field time.Time `json:"myName"` +// +// // Field appears in JSON as key "myName" and +// // the field is omitted from the object if its value is zero, +// // as defined above. +// Field time.Time `json:"myName,omitzero"` +// +// // Field appears in JSON as key "Field" (the default), but +// // the field is skipped if zero. +// // Note the leading comma. +// Field time.Time `json:",omitzero"` +// +// If both "omitempty" and "omitzero" are specified, the field will be omitted +// if the value is either empty or zero (or both). +// // The "string" option signals that a field is stored as JSON inside a // JSON-encoded string. It applies only to fields of string, floating point, // integer, or boolean types. This extra level of encoding is sometimes used @@ -318,6 +344,15 @@ func isEmptyValue(v reflect.Value) bool { return false } +func isZeroValue(v reflect.Value) bool { + if z, ok := v.Interface().(interface { + IsZero() bool + }); ok { + return z.IsZero() + } + return v.IsZero() +} + func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) { valueEncoder(v)(e, v, opts) } @@ -701,7 +736,8 @@ FieldLoop: fv = fv.Field(i) } - if f.omitEmpty && isEmptyValue(fv) { + if (f.omitEmpty && isEmptyValue(fv)) || + (f.omitZero && isZeroValue(fv)) { continue } e.WriteByte(next) @@ -1048,6 +1084,7 @@ type field struct { index []int typ reflect.Type omitEmpty bool + omitZero bool quoted bool encoder encoderFunc @@ -1154,6 +1191,7 @@ func typeFields(t reflect.Type) structFields { index: index, typ: ft, omitEmpty: opts.Contains("omitempty"), + omitZero: opts.Contains("omitzero"), quoted: quoted, } field.nameBytes = []byte(field.name) diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 23a14d0b172927..9083f1b3c33270 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -15,9 +15,10 @@ import ( "runtime/debug" "strconv" "testing" + "time" ) -type Optionals struct { +type OptionalsEmpty struct { Sr string `json:"sr"` So string `json:"so,omitempty"` Sw string `json:"-"` @@ -56,7 +57,7 @@ func TestOmitEmpty(t *testing.T) { "str": {}, "sto": {} }` - var o Optionals + var o OptionalsEmpty o.Sw = "something" o.Mr = map[string]any{} o.Mo = map[string]any{} @@ -70,6 +71,128 @@ func TestOmitEmpty(t *testing.T) { } } +type NonZeroStruct struct{} + +func (nzs NonZeroStruct) IsZero() bool { + return false +} + +type OptionalsZero struct { + Sr string `json:"sr"` + So string `json:"so,omitzero"` + Sw string `json:"-"` + + Ir int `json:"omitzero"` // actually named omitzero, not an option + Io int `json:"io,omitzero"` + + Slr []string `json:"slr,random"` + Slo []string `json:"slo,omitzero"` + SloNonNil []string `json:"slononnil,omitzero"` + + Mr map[string]any `json:"mr"` + Mo map[string]any `json:",omitzero"` + + Fr float64 `json:"fr"` + Fo float64 `json:"fo,omitzero"` + + Br bool `json:"br"` + Bo bool `json:"bo,omitzero"` + + Ur uint `json:"ur"` + Uo uint `json:"uo,omitzero"` + + Str struct{} `json:"str"` + Sto struct{} `json:"sto,omitzero"` + + Time time.Time `json:"time,omitzero"` + Nzs NonZeroStruct `json:"nzs,omitzero"` +} + +func TestOmitZero(t *testing.T) { + var want = `{ + "sr": "", + "omitzero": 0, + "slr": null, + "slononnil": [], + "mr": {}, + "Mo": {}, + "fr": 0, + "br": false, + "ur": 0, + "str": {}, + "nzs": {} +}` + var o OptionalsZero + o.Sw = "something" + o.SloNonNil = make([]string, 0) + o.Mr = map[string]any{} + o.Mo = map[string]any{} + + got, err := MarshalIndent(&o, "", " ") + if err != nil { + t.Fatalf("MarshalIndent error: %v", err) + } + if got := string(got); got != want { + t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", indentNewlines(got), indentNewlines(want)) + } +} + +type OptionalsEmptyZero struct { + Sr string `json:"sr"` + So string `json:"so,omitempty,omitzero"` + Sw string `json:"-"` + + Io int `json:"io,omitempty,omitzero"` + + Slr []string `json:"slr,random"` + Slo []string `json:"slo,omitempty,omitzero"` + SloNonNil []string `json:"slononnil,omitempty,omitzero"` + + Mr map[string]any `json:"mr"` + Mo map[string]any `json:",omitempty,omitzero"` + + Fr float64 `json:"fr"` + Fo float64 `json:"fo,omitempty,omitzero"` + + Br bool `json:"br"` + Bo bool `json:"bo,omitempty,omitzero"` + + Ur uint `json:"ur"` + Uo uint `json:"uo,omitempty,omitzero"` + + Str struct{} `json:"str"` + Sto struct{} `json:"sto,omitempty,omitzero"` + + Time time.Time `json:"time,omitempty,omitzero"` + Nzs NonZeroStruct `json:"nzs,omitzero"` +} + +func TestOmitEmptyZero(t *testing.T) { + var want = `{ + "sr": "", + "slr": null, + "mr": {}, + "fr": 0, + "br": false, + "ur": 0, + "str": {}, + "nzs": {} +}` + var o OptionalsEmptyZero + o.Sw = "something" + o.SloNonNil = make([]string, 0) + o.Mr = map[string]any{} + o.Mo = map[string]any{} + + got, err := MarshalIndent(&o, "", " ") + if err != nil { + t.Fatalf("MarshalIndent error: %v", err) + } + if got := string(got); got != want { + t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", indentNewlines(got), indentNewlines(want)) + } +} + type StringTag struct { BoolStr bool `json:",string"` IntStr int64 `json:",string"`