Skip to content

Commit

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

Change-Id: Idec483a03968cc671c8da27804589008b10864a1
  • Loading branch information
callthingsoff committed Sep 25, 2024
1 parent b17a55d commit aacdc51
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 3 deletions.
11 changes: 11 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,11 @@
When marshaling, the `omitzero` option specifies that the struct field should be
omitted if the field value is zero as determined by the `IsZero() bool` method
if present, otherwise based on whether the field is the zero Go value (according
to [reflect.Value.IsZero]).

This option has no effect when unmarshaling. If `omitempty` is specified together
with `omitzero`, whether a field is omitted is based on the logical OR of the two.

This will mean that `omitzero` of a slice omits a nil slice but emits [] for a
zero-length non-nil slice (and similar for maps). It will also mean that
`omitzero` of a [time.Time] omits time.Time{}.
16 changes: 15 additions & 1 deletion src/encoding/json/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ func isEmptyValue(v reflect.Value) bool {
return false
}

type zeroable interface {
IsZero() bool
}

func isZeroValue(v reflect.Value) bool {
if z, ok := v.Interface().(zeroable); 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 +712,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 +1060,7 @@ type field struct {
index []int
typ reflect.Type
omitEmpty bool
omitZero bool
quoted bool

encoder encoderFunc
Expand Down Expand Up @@ -1154,6 +1167,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
119 changes: 117 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,120 @@ func TestOmitEmpty(t *testing.T) {
}
}

type OptionalsZero struct {
Sr string `json:"sr"`
So string `json:"so,omitzero"`
Sw string `json:"-"`

Ir int `json:"omitempty"` // actually named omitempty, 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"`

MyTime time.Time `json:"mytime,omitzero"`
}

func TestOmitZero(t *testing.T) {
var want = `{
"sr": "",
"omitempty": 0,
"slr": null,
"slononnil": [],
"mr": {},
"Mo": {},
"fr": 0,
"br": false,
"ur": 0,
"str": {}
}`
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:"-"`

Ir int `json:"omitempty"` // actually named omitempty, not an option
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"`

MyTime time.Time `json:"mytime,omitempty,omitzero"`
}

func TestOmitEmptyZero(t *testing.T) {
var want = `{
"sr": "",
"omitempty": 0,
"slr": null,
"mr": {},
"fr": 0,
"br": false,
"ur": 0,
"str": {}
}`
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 aacdc51

Please sign in to comment.