Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

encoding/json: add omitzero option #69622

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/next/6-stdlib/99-minor/encoding/json/45669.md
Original file line number Diff line number Diff line change
@@ -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).
57 changes: 56 additions & 1 deletion src/encoding/json/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
188 changes: 185 additions & 3 deletions src/encoding/json/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -45,7 +46,7 @@ type Optionals struct {
}

func TestOmitEmpty(t *testing.T) {
var want = `{
const want = `{
"sr": "",
"omitempty": 0,
"slr": null,
Expand All @@ -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{}
Expand All @@ -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"`
Expand Down