1
0
mirror of https://github.com/golang/go synced 2024-11-22 04:54:42 -07:00

encoding/json: add omitzero option

Fixes #45669

Change-Id: Ic13523c0b3acdfc5b3e29a717bc62fde302ed8fd
GitHub-Last-Rev: 57030f26b0
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>
This commit is contained in:
Jes Cok 2024-10-01 03:15:08 +00:00 committed by Gopher Robot
parent 41d189a3f6
commit e86982c515
3 changed files with 248 additions and 4 deletions

View File

@ -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).

View 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,

View 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"`