]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: add omitzero option
authorJes Cok <xigua67damn@gmail.com>
Tue, 1 Oct 2024 03:15:08 +0000 (03:15 +0000)
committerGopher Robot <gobot@golang.org>
Wed, 2 Oct 2024 14:22:27 +0000 (14:22 +0000)
Fixes #45669

Change-Id: Ic13523c0b3acdfc5b3e29a717bc62fde302ed8fd
GitHub-Last-Rev: 57030f26b0062fa8eda21b3a73b7665deab88c76
GitHub-Pull-Request: golang/go#69622
Reviewed-on: https://go-review.googlesource.com/c/go/+/615676
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Joseph Tsai <joetsai@digital-static.net>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
doc/next/6-stdlib/99-minor/encoding/json/45669.md [new file with mode: 0644]
src/encoding/json/encode.go
src/encoding/json/encode_test.go

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 (file)
index 0000000..1d62ddc
--- /dev/null
@@ -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).
index 988de716124862593e56d20854e7655c610de82d..02522f088a070c6b58991895c03ea477cd7d5dc7 100644 (file)
@@ -99,6 +99,17 @@ 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 rules:
+//
+// 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.
+//
+// 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
@@ -701,7 +712,8 @@ FieldLoop:
                        fv = fv.Field(i)
                }
 
-               if f.omitEmpty && isEmptyValue(fv) {
+               if (f.omitEmpty && isEmptyValue(fv)) ||
+                       (f.omitZero && (f.isZero == nil && fv.IsZero() || (f.isZero != nil && f.isZero(fv)))) {
                        continue
                }
                e.WriteByte(next)
@@ -1048,11 +1060,19 @@ type field struct {
        index     []int
        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.
@@ -1154,6 +1174,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)
@@ -1163,6 +1184,40 @@ 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,
index 23a14d0b1729279b3b657f23b5cd88ee542f90f3..79c481754e005748cb8c14c371a0fe7176961a99 100644 (file)
@@ -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:"-"`
@@ -45,7 +46,7 @@ type Optionals struct {
 }
 
 func TestOmitEmpty(t *testing.T) {
-       var want = `{
+       const want = `{
  "sr": "",
  "omitempty": 0,
  "slr": null,
@@ -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,187 @@ func TestOmitEmpty(t *testing.T) {
        }
 }
 
+type NonZeroStruct struct{}
+
+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"`
+       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"`
+       Moo map[string]any `json:"moo,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"`
+
+       Ur uint `json:"ur"`
+       Uo uint `json:"uo,omitzero"`
+
+       Str struct{} `json:"str"`
+       Sto struct{} `json:"sto,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"`           // non-nil pointer
+       NoPanicStruct4 NoPanicStruct  `json:"nps4,omitzero"`           // concrete type
+}
+
+func TestOmitZero(t *testing.T) {
+       const want = `{
+ "sr": "",
+ "omitzero": 0,
+ "slr": null,
+ "slononnil": [],
+ "mr": {},
+ "Mo": {},
+ "fr": 0,
+ "br": false,
+ "ur": 0,
+ "str": {},
+ "nzs": {},
+ "nps1": {},
+ "nps3": {},
+ "nps4": {}
+}`
+       var o OptionalsZero
+       o.Sw = "something"
+       o.SloNonNil = make([]string, 0)
+       o.Mr = map[string]any{}
+       o.Mo = map[string]any{}
+
+       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 {
+               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))
+       }
+}
+
+func TestOmitZeroMap(t *testing.T) {
+       const want = `{
+ "foo": {
+  "sr": "",
+  "omitzero": 0,
+  "slr": null,
+  "mr": null,
+  "fr": 0,
+  "br": false,
+  "ur": 0,
+  "str": {},
+  "nzs": {},
+  "nps4": {}
+ }
+}`
+       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"`
+       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,omitempty,omitzero"`
+}
+
+func TestOmitEmptyZero(t *testing.T) {
+       const 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"`