From bfb9b41af77521e205dbcf2efdfe087db9fa79db Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Wed, 25 Sep 2024 19:48:52 +0800 Subject: [PATCH 01/11] 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"` From f92a355ec46484084b6e4227acc7e63bfec541ea Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Thu, 26 Sep 2024 21:04:32 +0800 Subject: [PATCH 02/11] 1 Change-Id: I38fcb3694c3fbd98eef600c8c32f982122e3fe8f --- src/encoding/json/encode_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 9083f1b3c33270..9253bb78e70ca0 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -164,7 +164,7 @@ type OptionalsEmptyZero struct { Sto struct{} `json:"sto,omitempty,omitzero"` Time time.Time `json:"time,omitempty,omitzero"` - Nzs NonZeroStruct `json:"nzs,omitzero"` + Nzs NonZeroStruct `json:"nzs,omitempty,omitzero"` } func TestOmitEmptyZero(t *testing.T) { From 8ce1d4f55d04da043ed5e1852d893b0c65472f86 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Thu, 26 Sep 2024 21:10:41 +0800 Subject: [PATCH 03/11] 2 Change-Id: I0d330c14e2a5d16be51cf1260fc5df4fc8eb9cc2 --- src/encoding/json/encode.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index e31746fbef3fc3..90e2886bb69cd8 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -100,7 +100,7 @@ import ( // 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: +// from the encoding if the field has a zero value, according to rules: // // 1) If the field type has an "IsZero() bool" method, that will be used to // determine whether the value is zero. @@ -114,7 +114,7 @@ import ( // // // Field appears in JSON as key "myName" and // // the field is omitted from the object if its value is zero, -// // as defined above. +// // as determined above. // Field time.Time `json:"myName,omitzero"` // // // Field appears in JSON as key "Field" (the default), but From 956bcd2963222dd5b981090f4cb66b547bad9c31 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Thu, 26 Sep 2024 22:28:10 +0800 Subject: [PATCH 04/11] 3 Change-Id: Ia6f6f4b1812ca06e5a1863610cfc18a0c82a7a0c --- src/encoding/json/encode.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 90e2886bb69cd8..65c15807f6cac8 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -107,21 +107,6 @@ import ( // // 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 determined 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). // From 7314dafb6b36c533de070ebc777e9d17ad66f3b8 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Fri, 27 Sep 2024 21:10:35 +0800 Subject: [PATCH 05/11] 4 Change-Id: I84c74a84f5d69fcd9abd5933ca2923dcc8ff2220 --- src/encoding/json/encode.go | 34 +++++++++++++++++++++++++++----- src/encoding/json/encode_test.go | 14 +++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 65c15807f6cac8..10fefc50165633 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -329,13 +329,37 @@ func isEmptyValue(v reflect.Value) bool { return false } +type isZeroer interface { + IsZero() bool +} + +var isZeroerType = reflect.TypeFor[isZeroer]() + func isZeroValue(v reflect.Value) bool { - if z, ok := v.Interface().(interface { - IsZero() bool - }); ok { - return z.IsZero() + // Provide a function that uses a type's IsZero method. + var isZero func(reflect.Value) bool + + switch { + case v.Kind() == reflect.Interface && v.Type().Implements(isZeroerType): + isZero = func(va reflect.Value) bool { + // Avoid panics calling IsZero on a nil interface or + // non-nil interface with nil pointer. + return va.IsNil() || + (va.Elem().Kind() == reflect.Pointer && va.Elem().IsNil()) || + va.Interface().(isZeroer).IsZero() + } + case v.Kind() == reflect.Pointer && v.Type().Implements(isZeroerType): + isZero = func(va reflect.Value) bool { + // Avoid panics calling IsZero on nil pointer. + return va.IsNil() || va.Interface().(isZeroer).IsZero() + } + case v.Type().Implements(isZeroerType): + isZero = func(va reflect.Value) bool { return va.Interface().(isZeroer).IsZero() } + case reflect.PointerTo(v.Type()).Implements(isZeroerType): + isZero = func(va reflect.Value) bool { return va.Addr().Interface().(isZeroer).IsZero() } } - return v.IsZero() + + return (isZero == nil && v.IsZero() || (isZero != nil && isZero(v))) } func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) { diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 9253bb78e70ca0..9154054f7e4597 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -77,6 +77,14 @@ func (nzs NonZeroStruct) IsZero() bool { return false } +type NoPanicStruct struct { + Int int `json:"int,omitzero"` +} + +func (nps *NoPanicStruct) IsZero() bool { + return nps.Int == 0 +} + type OptionalsZero struct { Sr string `json:"sr"` So string `json:"so,omitzero"` @@ -106,6 +114,12 @@ type OptionalsZero struct { Time time.Time `json:"time,omitzero"` Nzs NonZeroStruct `json:"nzs,omitzero"` + + IsZeroer interface { + IsZero() bool + } `json:"iszeroer,omitzero"` + NoPanicStruct1 *NoPanicStruct `json:"nps1,omitzero"` + NoPanicStruct2 NoPanicStruct `json:"nps2,omitzero"` } func TestOmitZero(t *testing.T) { From b3d28f68d7faeda4b83d0aae17134bd4ac85643a Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Sat, 28 Sep 2024 14:10:01 +0800 Subject: [PATCH 06/11] 5 Change-Id: I3d8ffdfaf3478adcaca09615908f6f9b065165fc --- src/encoding/json/encode.go | 20 ++++++++++---------- src/encoding/json/encode_test.go | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 10fefc50165633..d58a0220d95cd3 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -337,29 +337,29 @@ var isZeroerType = reflect.TypeFor[isZeroer]() func isZeroValue(v reflect.Value) bool { // Provide a function that uses a type's IsZero method. - var isZero func(reflect.Value) bool + var isZero func() bool switch { case v.Kind() == reflect.Interface && v.Type().Implements(isZeroerType): - isZero = func(va reflect.Value) bool { + isZero = func() bool { // Avoid panics calling IsZero on a nil interface or // non-nil interface with nil pointer. - return va.IsNil() || - (va.Elem().Kind() == reflect.Pointer && va.Elem().IsNil()) || - va.Interface().(isZeroer).IsZero() + return v.IsNil() || + (v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) || + v.Interface().(isZeroer).IsZero() } case v.Kind() == reflect.Pointer && v.Type().Implements(isZeroerType): - isZero = func(va reflect.Value) bool { + isZero = func() bool { // Avoid panics calling IsZero on nil pointer. - return va.IsNil() || va.Interface().(isZeroer).IsZero() + return v.IsNil() || v.Interface().(isZeroer).IsZero() } case v.Type().Implements(isZeroerType): - isZero = func(va reflect.Value) bool { return va.Interface().(isZeroer).IsZero() } + isZero = func() bool { return v.Interface().(isZeroer).IsZero() } case reflect.PointerTo(v.Type()).Implements(isZeroerType): - isZero = func(va reflect.Value) bool { return va.Addr().Interface().(isZeroer).IsZero() } + isZero = func() bool { return v.Addr().Interface().(isZeroer).IsZero() } } - return (isZero == nil && v.IsZero() || (isZero != nil && isZero(v))) + return (isZero == nil && v.IsZero() || (isZero != nil && isZero())) } func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) { diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 9154054f7e4597..87094d80514650 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -85,6 +85,10 @@ func (nps *NoPanicStruct) IsZero() bool { return nps.Int == 0 } +type IsZeroer interface { + IsZero() bool +} + type OptionalsZero struct { Sr string `json:"sr"` So string `json:"so,omitzero"` @@ -115,11 +119,11 @@ type OptionalsZero struct { Time time.Time `json:"time,omitzero"` Nzs NonZeroStruct `json:"nzs,omitzero"` - IsZeroer interface { - IsZero() bool - } `json:"iszeroer,omitzero"` - NoPanicStruct1 *NoPanicStruct `json:"nps1,omitzero"` - NoPanicStruct2 NoPanicStruct `json:"nps2,omitzero"` + NilIsZeroer IsZeroer `json:"niliszeroer,omitzero"` // nil interface + NonNilIsZeroer IsZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface + NoPanicStruct1 IsZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer + NoPanicStruct2 *NoPanicStruct `json:"nps2,omitzero"` // nil pointer + NoPanicStruct3 NoPanicStruct `json:"nps3,omitzero"` // concrete type } func TestOmitZero(t *testing.T) { @@ -142,6 +146,9 @@ func TestOmitZero(t *testing.T) { o.Mr = map[string]any{} o.Mo = map[string]any{} + o.NonNilIsZeroer = time.Time{} + o.NoPanicStruct1 = &NoPanicStruct{} + got, err := MarshalIndent(&o, "", " ") if err != nil { t.Fatalf("MarshalIndent error: %v", err) From 92a4ba35e177f4dd0fec21eacab1ec1f9ac7b9a1 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Sat, 28 Sep 2024 14:20:02 +0800 Subject: [PATCH 07/11] 6 Change-Id: I94aaf74a58984f022c6078ca39f85c761c7607e4 --- src/encoding/json/encode_test.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 87094d80514650..a712f481911447 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -85,10 +85,6 @@ func (nps *NoPanicStruct) IsZero() bool { return nps.Int == 0 } -type IsZeroer interface { - IsZero() bool -} - type OptionalsZero struct { Sr string `json:"sr"` So string `json:"so,omitzero"` @@ -119,9 +115,9 @@ type OptionalsZero struct { Time time.Time `json:"time,omitzero"` Nzs NonZeroStruct `json:"nzs,omitzero"` - NilIsZeroer IsZeroer `json:"niliszeroer,omitzero"` // nil interface - NonNilIsZeroer IsZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface - NoPanicStruct1 IsZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer + NilIsZeroer isZeroer `json:"niliszeroer,omitzero"` // nil interface + NonNilIsZeroer isZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface + NoPanicStruct1 isZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer NoPanicStruct2 *NoPanicStruct `json:"nps2,omitzero"` // nil pointer NoPanicStruct3 NoPanicStruct `json:"nps3,omitzero"` // concrete type } From 1270b2f17a7ab7e377c281d321c5757c80730a08 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Sun, 29 Sep 2024 22:16:56 +0800 Subject: [PATCH 08/11] 7 Change-Id: Ia04b14d1c08fd8c9bce0c36d526bf68f7c5eced1 --- src/encoding/json/encode.go | 14 ++++++++-- src/encoding/json/encode_test.go | 47 +++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index d58a0220d95cd3..a080391e85a234 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -354,9 +354,19 @@ func isZeroValue(v reflect.Value) bool { return v.IsNil() || v.Interface().(isZeroer).IsZero() } case v.Type().Implements(isZeroerType): - isZero = func() bool { return v.Interface().(isZeroer).IsZero() } + isZero = func() bool { + return v.Interface().(isZeroer).IsZero() + } case reflect.PointerTo(v.Type()).Implements(isZeroerType): - isZero = func() bool { return v.Addr().Interface().(isZeroer).IsZero() } + isZero = func() bool { + if !v.CanAddr() { + // Temporarily box v so we can take the address. + v2 := reflect.New(v.Type()).Elem() + v2.Set(v) + v = v2 + } + return v.Addr().Interface().(isZeroer).IsZero() + } } return (isZero == nil && v.IsZero() || (isZero != nil && isZero())) diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index a712f481911447..cd20dce998774f 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -46,7 +46,7 @@ type OptionalsEmpty struct { } func TestOmitEmpty(t *testing.T) { - var want = `{ + const want = `{ "sr": "", "omitempty": 0, "slr": null, @@ -82,7 +82,7 @@ type NoPanicStruct struct { } func (nps *NoPanicStruct) IsZero() bool { - return nps.Int == 0 + return nps.Int != 0 } type OptionalsZero struct { @@ -100,8 +100,10 @@ type OptionalsZero struct { Mr map[string]any `json:"mr"` Mo map[string]any `json:",omitzero"` - Fr float64 `json:"fr"` - Fo float64 `json:"fo,omitzero"` + Fr float64 `json:"fr"` + Fo float64 `json:"fo,omitzero"` + Foo float64 `json:"foo,omitzero"` + Foo2 [2]float64 `json:"foo2,omitzero"` Br bool `json:"br"` Bo bool `json:"bo,omitzero"` @@ -123,7 +125,7 @@ type OptionalsZero struct { } func TestOmitZero(t *testing.T) { - var want = `{ + const want = `{ "sr": "", "omitzero": 0, "slr": null, @@ -134,7 +136,9 @@ func TestOmitZero(t *testing.T) { "br": false, "ur": 0, "str": {}, - "nzs": {} + "nzs": {}, + "nps1": {}, + "nps3": {} }` var o OptionalsZero o.Sw = "something" @@ -142,6 +146,9 @@ func TestOmitZero(t *testing.T) { o.Mr = map[string]any{} o.Mo = map[string]any{} + o.Foo = -0 + o.Foo2 = [2]float64{0, -0} + o.NonNilIsZeroer = time.Time{} o.NoPanicStruct1 = &NoPanicStruct{} @@ -154,6 +161,32 @@ func TestOmitZero(t *testing.T) { } } +func TestOmitZeroMap(t *testing.T) { + const want = `{ + "foo": { + "sr": "", + "omitzero": 0, + "slr": null, + "mr": null, + "fr": 0, + "br": false, + "ur": 0, + "str": {}, + "nzs": {}, + "nps3": {} + } +}` + m := map[string]OptionalsZero{"foo": {}} + got, err := MarshalIndent(m, "", " ") + if err != nil { + t.Fatalf("MarshalIndent error: %v", err) + } + if got := string(got); got != want { + fmt.Println(got) + 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"` @@ -185,7 +218,7 @@ type OptionalsEmptyZero struct { } func TestOmitEmptyZero(t *testing.T) { - var want = `{ + const want = `{ "sr": "", "slr": null, "mr": {}, From b5a7e68848b24b5a4cfa1f890993a5d1f459b8a2 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Sun, 29 Sep 2024 22:45:44 +0800 Subject: [PATCH 09/11] 8 Change-Id: Iade683ed5c91d148096e4b80f8c5807697afc134 --- src/encoding/json/encode.go | 84 +++++++++++++++----------------- src/encoding/json/encode_test.go | 2 +- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index a080391e85a234..55fca6e0e1bff7 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -329,49 +329,6 @@ func isEmptyValue(v reflect.Value) bool { return false } -type isZeroer interface { - IsZero() bool -} - -var isZeroerType = reflect.TypeFor[isZeroer]() - -func isZeroValue(v reflect.Value) bool { - // Provide a function that uses a type's IsZero method. - var isZero func() bool - - switch { - case v.Kind() == reflect.Interface && v.Type().Implements(isZeroerType): - isZero = func() bool { - // Avoid panics calling IsZero on a nil interface or - // non-nil interface with nil pointer. - return v.IsNil() || - (v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) || - v.Interface().(isZeroer).IsZero() - } - case v.Kind() == reflect.Pointer && v.Type().Implements(isZeroerType): - isZero = func() bool { - // Avoid panics calling IsZero on nil pointer. - return v.IsNil() || v.Interface().(isZeroer).IsZero() - } - case v.Type().Implements(isZeroerType): - isZero = func() bool { - return v.Interface().(isZeroer).IsZero() - } - case reflect.PointerTo(v.Type()).Implements(isZeroerType): - isZero = func() bool { - if !v.CanAddr() { - // Temporarily box v so we can take the address. - v2 := reflect.New(v.Type()).Elem() - v2.Set(v) - v = v2 - } - return v.Addr().Interface().(isZeroer).IsZero() - } - } - - return (isZero == nil && v.IsZero() || (isZero != nil && isZero())) -} - func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) { valueEncoder(v)(e, v, opts) } @@ -756,7 +713,7 @@ FieldLoop: } if (f.omitEmpty && isEmptyValue(fv)) || - (f.omitZero && isZeroValue(fv)) { + (f.omitZero && (f.isZero == nil && fv.IsZero() || (f.isZero != nil && f.isZero(fv)))) { continue } e.WriteByte(next) @@ -1104,11 +1061,18 @@ type field struct { typ reflect.Type omitEmpty bool omitZero bool + isZero func(reflect.Value) bool quoted bool encoder encoderFunc } +type isZeroer interface { + IsZero() bool +} + +var isZeroerType = reflect.TypeFor[isZeroer]() + // typeFields returns a list of fields that JSON should recognize for the given type. // The algorithm is breadth-first search over the set of structs to include - the top struct // and then any reachable anonymous structs. @@ -1220,6 +1184,38 @@ func typeFields(t reflect.Type) structFields { field.nameEscHTML = `"` + string(nameEscBuf) + `":` field.nameNonEsc = `"` + field.name + `":` + if field.omitZero { + t := sf.Type + // Provide a function that uses a type's IsZero method. + switch { + case t.Kind() == reflect.Interface && t.Implements(isZeroerType): + field.isZero = func(v reflect.Value) bool { + // Avoid panics calling IsZero on a nil interface or + // non-nil interface with nil pointer. + return v.IsNil() || (v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) || v.Interface().(isZeroer).IsZero() + } + case t.Kind() == reflect.Pointer && t.Implements(isZeroerType): + field.isZero = func(v reflect.Value) bool { + // Avoid panics calling IsZero on nil pointer. + return v.IsNil() || v.Interface().(isZeroer).IsZero() + } + case t.Implements(isZeroerType): + field.isZero = func(v reflect.Value) bool { + return v.Interface().(isZeroer).IsZero() + } + case reflect.PointerTo(t).Implements(isZeroerType): + field.isZero = func(v reflect.Value) bool { + if !v.CanAddr() { + // Temporarily box v so we can take the address. + v2 := reflect.New(v.Type()).Elem() + v2.Set(v) + v = v2 + } + return v.Addr().Interface().(isZeroer).IsZero() + } + } + } + fields = append(fields, field) if count[f.typ] > 1 { // If there were multiple instances, add a second, diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index cd20dce998774f..b9d240221d5624 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -147,7 +147,7 @@ func TestOmitZero(t *testing.T) { o.Mo = map[string]any{} o.Foo = -0 - o.Foo2 = [2]float64{0, -0} + o.Foo2 = [2]float64{+0, -0} o.NonNilIsZeroer = time.Time{} o.NoPanicStruct1 = &NoPanicStruct{} From 0b672df57ea5e310df48b70b8572e220b4c81f1d Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Tue, 1 Oct 2024 11:01:12 +0800 Subject: [PATCH 10/11] 9 Change-Id: I49a2c48613ea2b06dd119398279e82683559de59 --- src/encoding/json/encode_test.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index b9d240221d5624..79c481754e0057 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -97,8 +97,9 @@ type OptionalsZero struct { Slo []string `json:"slo,omitzero"` SloNonNil []string `json:"slononnil,omitzero"` - Mr map[string]any `json:"mr"` - Mo map[string]any `json:",omitzero"` + Mr map[string]any `json:"mr"` + Mo map[string]any `json:",omitzero"` + Moo map[string]any `json:"moo,omitzero"` Fr float64 `json:"fr"` Fo float64 `json:"fo,omitzero"` @@ -114,14 +115,17 @@ type OptionalsZero struct { Str struct{} `json:"str"` Sto struct{} `json:"sto,omitzero"` - Time time.Time `json:"time,omitzero"` - Nzs NonZeroStruct `json:"nzs,omitzero"` + Time time.Time `json:"time,omitzero"` + TimeLocal time.Time `json:"timelocal,omitzero"` + Nzs NonZeroStruct `json:"nzs,omitzero"` NilIsZeroer isZeroer `json:"niliszeroer,omitzero"` // nil interface NonNilIsZeroer isZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface + NoPanicStruct0 isZeroer `json:"nps0,omitzero"` // non-nil interface with nil pointer NoPanicStruct1 isZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer NoPanicStruct2 *NoPanicStruct `json:"nps2,omitzero"` // nil pointer - NoPanicStruct3 NoPanicStruct `json:"nps3,omitzero"` // concrete type + NoPanicStruct3 *NoPanicStruct `json:"nps3,omitzero"` // non-nil pointer + NoPanicStruct4 NoPanicStruct `json:"nps4,omitzero"` // concrete type } func TestOmitZero(t *testing.T) { @@ -138,7 +142,8 @@ func TestOmitZero(t *testing.T) { "str": {}, "nzs": {}, "nps1": {}, - "nps3": {} + "nps3": {}, + "nps4": {} }` var o OptionalsZero o.Sw = "something" @@ -149,8 +154,12 @@ func TestOmitZero(t *testing.T) { o.Foo = -0 o.Foo2 = [2]float64{+0, -0} + o.TimeLocal = time.Time{}.Local() + o.NonNilIsZeroer = time.Time{} + o.NoPanicStruct0 = (*NoPanicStruct)(nil) o.NoPanicStruct1 = &NoPanicStruct{} + o.NoPanicStruct3 = &NoPanicStruct{} got, err := MarshalIndent(&o, "", " ") if err != nil { @@ -173,7 +182,7 @@ func TestOmitZeroMap(t *testing.T) { "ur": 0, "str": {}, "nzs": {}, - "nps3": {} + "nps4": {} } }` m := map[string]OptionalsZero{"foo": {}} From 57030f26b0062fa8eda21b3a73b7665deab88c76 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Tue, 1 Oct 2024 11:07:23 +0800 Subject: [PATCH 11/11] 10 Change-Id: I49f14328841cc2e950e244f4cd98c4dcea1f0987 --- src/encoding/json/encode.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 55fca6e0e1bff7..02522f088a070c 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -1192,7 +1192,9 @@ func typeFields(t reflect.Type) structFields { field.isZero = func(v reflect.Value) bool { // Avoid panics calling IsZero on a nil interface or // non-nil interface with nil pointer. - return v.IsNil() || (v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) || v.Interface().(isZeroer).IsZero() + return v.IsNil() || + (v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) || + v.Interface().(isZeroer).IsZero() } case t.Kind() == reflect.Pointer && t.Implements(isZeroerType): field.isZero = func(v reflect.Value) bool {