diff --git a/doc/next/6-stdlib/99-minor/encoding/json/45669.md b/doc/next/6-stdlib/99-minor/encoding/json/45669.md new file mode 100644 index 00000000000000..ec18ccecfffbab --- /dev/null +++ b/doc/next/6-stdlib/99-minor/encoding/json/45669.md @@ -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{}. \ No newline at end of file diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 988de716124862..671cd3f648fb68 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -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) } @@ -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) @@ -1048,6 +1060,7 @@ type field struct { index []int typ reflect.Type omitEmpty bool + omitZero bool quoted bool encoder encoderFunc @@ -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) diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 23a14d0b172927..388d4dd913baa2 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -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:"-"` @@ -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,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"`