// // 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
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)
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.
index: index,
typ: ft,
omitEmpty: opts.Contains("omitempty"),
+ omitZero: opts.Contains("omitzero"),
quoted: quoted,
}
field.nameBytes = []byte(field.name)
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,
"runtime/debug"
"strconv"
"testing"
+ "time"
)
-type Optionals struct {
+type OptionalsEmpty struct {
Sr string `json:"sr"`
So string `json:"so,omitempty"`
Sw string `json:"-"`
}
func TestOmitEmpty(t *testing.T) {
- var want = `{
+ const want = `{
"sr": "",
"omitempty": 0,
"slr": null,
"str": {},
"sto": {}
}`
- var o Optionals
+ var o OptionalsEmpty
o.Sw = "something"
o.Mr = map[string]any{}
o.Mo = map[string]any{}
}
}
+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"`