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:
parent
41d189a3f6
commit
e86982c515
7
doc/next/6-stdlib/99-minor/encoding/json/45669.md
Normal file
7
doc/next/6-stdlib/99-minor/encoding/json/45669.md
Normal 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).
|
@ -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,
|
||||
|
@ -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"`
|
||||
|
Loading…
Reference in New Issue
Block a user