Skip to content

Commit

Permalink
encoding/json: add omitzero option
Browse files Browse the repository at this point in the history
Fixes #45669

Change-Id: Idec483a03968cc671c8da27804589008b10864a1
  • Loading branch information
callthingsoff committed Sep 26, 2024
1 parent 8014360 commit 3de4cb2
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 3 deletions.
8 changes: 8 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,8 @@
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).

40 changes: 39 additions & 1 deletion src/encoding/json/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1048,6 +1084,7 @@ type field struct {
index []int
typ reflect.Type
omitEmpty bool
omitZero bool
quoted bool

encoder encoderFunc
Expand Down Expand Up @@ -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)
Expand Down
127 changes: 125 additions & 2 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 @@ -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,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"`
Expand Down

0 comments on commit 3de4cb2

Please sign in to comment.