Skip to content

encoding/xml: add omitzero option #74579

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions src/encoding/xml/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,11 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat
return nil
}

if finfo != nil && finfo.flags&fOmitZero != 0 &&
(finfo.isZero == nil && val.IsZero() || (finfo.isZero != nil && finfo.isZero(val))) {
return nil
}

// Drill into interfaces and pointers.
// This can turn into an infinite loop given a cyclic chain,
// but it matches the Go 1 behavior.
Expand Down Expand Up @@ -535,6 +540,11 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat
continue
}

if finfo.flags&fOmitZero != 0 && (!fv.IsValid() ||
(finfo.isZero == nil && fv.IsZero() || (finfo.isZero != nil && finfo.isZero(fv)))) {
continue
}

if fv.Kind() == reflect.Interface && fv.IsNil() {
continue
}
Expand Down
188 changes: 188 additions & 0 deletions src/encoding/xml/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2589,3 +2589,191 @@ func TestClose(t *testing.T) {
})
}
}

type OptionalsEmpty struct {
Sr string `xml:"sr"`
So string `xml:"so,omitempty"`
Sw string `xml:"-"`

Ir int `xml:"omitempty"` // actually named omitempty, not an option
Io int `xml:"io,omitempty"`

Slr []string `xml:"slr,random"`
Slo []string `xml:"slo,omitempty"`

Fr float64 `xml:"fr"`
Fo float64 `xml:"fo,omitempty"`

Br bool `xml:"br"`
Bo bool `xml:"bo,omitempty"`

Ur uint `xml:"ur"`
Uo uint `xml:"uo,omitempty"`

Str struct{} `xml:"str"`
Sto struct{} `xml:"sto,omitempty"`
}

func TestOmitEmpty(t *testing.T) {
const want = `<OptionalsEmpty>
<sr></sr>
<omitempty>0</omitempty>
<fr>0</fr>
<br>false</br>
<ur>0</ur>
<str></str>
<sto></sto>
</OptionalsEmpty>`
var o OptionalsEmpty
o.Sw = "something"

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 NonZeroStruct struct{}

func (nzs NonZeroStruct) IsZero() bool {
return false
}

type NoPanicStruct struct {
Int int `xml:"int,omitzero"`
}

func (nps *NoPanicStruct) IsZero() bool {
return nps.Int != 0
}

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

Ir int `xml:"omitzero"` // actually named omitzero, not an option
Io int `xml:"io,omitzero"`

Slr []string `xml:"slr,random"`
Slo []string `xml:"slo,omitzero"`
SloNonNil []string `xml:"slononnil,omitzero"`

Fr float64 `xml:"fr"`
Fo float64 `xml:"fo,omitzero"`
Foo float64 `xml:"foo,omitzero"`
Foo2 [2]float64 `xml:"foo2,omitzero"`

Br bool `xml:"br"`
Bo bool `xml:"bo,omitzero"`

Ur uint `xml:"ur"`
Uo uint `xml:"uo,omitzero"`

Str struct{} `xml:"str"`
Sto struct{} `xml:"sto,omitzero"`

Time time.Time `xml:"time,omitzero"`
TimeLocal time.Time `xml:"timelocal,omitzero"`
Nzs NonZeroStruct `xml:"nzs,omitzero"`

NilIsZeroer isZeroer `xml:"niliszeroer,omitzero"` // nil interface
NonNilIsZeroer isZeroer `xml:"nonniliszeroer,omitzero"` // non-nil interface
NoPanicStruct0 isZeroer `xml:"nps0,omitzero"` // non-nil interface with nil pointer
NoPanicStruct1 isZeroer `xml:"nps1,omitzero"` // non-nil interface with non-nil pointer
NoPanicStruct2 *NoPanicStruct `xml:"nps2,omitzero"` // nil pointer
NoPanicStruct3 *NoPanicStruct `xml:"nps3,omitzero"` // non-nil pointer
NoPanicStruct4 NoPanicStruct `xml:"nps4,omitzero"` // concrete type
}

func TestOmitZero(t *testing.T) {
const want = `<OptionalsZero>
<sr></sr>
<omitzero>0</omitzero>
<fr>0</fr>
<br>false</br>
<ur>0</ur>
<str></str>
<nzs></nzs>
<nps1></nps1>
<nps3></nps3>
<nps4></nps4>
</OptionalsZero>`
var o OptionalsZero
o.Sw = "something"
o.SloNonNil = make([]string, 0)

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))
}
}

type OptionalsEmptyZero struct {
Sr string `xml:"sr"`
So string `xml:"so,omitempty,omitzero"`
Sw string `xml:"-"`

Io int `xml:"io,omitempty,omitzero"`

Slr []string `xml:"slr,random"`
Slo []string `xml:"slo,omitempty,omitzero"`
SloNonNil []string `xml:"slononnil,omitempty,omitzero"`

Fr float64 `xml:"fr"`
Fo float64 `xml:"fo,omitempty,omitzero"`

Br bool `xml:"br"`
Bo bool `xml:"bo,omitempty,omitzero"`

Ur uint `xml:"ur"`
Uo uint `xml:"uo,omitempty,omitzero"`

Str struct{} `xml:"str"`
Sto struct{} `xml:"sto,omitempty,omitzero"`

Time time.Time `xml:"time,omitempty,omitzero"`
Nzs NonZeroStruct `xml:"nzs,omitempty,omitzero"`
}

func TestOmitEmptyZero(t *testing.T) {
const want = `<OptionalsEmptyZero>
<sr></sr>
<fr>0</fr>
<br>false</br>
<ur>0</ur>
<str></str>
<nzs></nzs>
</OptionalsEmptyZero>`
var o OptionalsEmptyZero
o.Sw = "something"
o.SloNonNil = make([]string, 0)

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 indentNewlines(s string) string {
return strings.Join(strings.Split(s, "\n"), "\n\t")
}
44 changes: 43 additions & 1 deletion src/encoding/xml/typeinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type fieldInfo struct {
xmlns string
flags fieldFlags
parents []string
isZero func(reflect.Value) bool
}

type fieldFlags int
Expand All @@ -38,6 +39,7 @@ const (
fAny

fOmitEmpty
fOmitZero

fMode = fElement | fAttr | fCDATA | fCharData | fInnerXML | fComment | fAny

Expand Down Expand Up @@ -109,6 +111,12 @@ func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
return ti.(*typeInfo), nil
}

type isZeroer interface {
IsZero() bool
}

var isZeroerType = reflect.TypeFor[isZeroer]()

// structFieldInfo builds and returns a fieldInfo for f.
func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
finfo := &fieldInfo{idx: f.Index}
Expand Down Expand Up @@ -141,6 +149,39 @@ func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, erro
finfo.flags |= fAny
case "omitempty":
finfo.flags |= fOmitEmpty
case "omitzero":
finfo.flags |= fOmitZero
t := f.Type
// Provide a function that uses a type's IsZero method.
switch {
case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
finfo.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):
finfo.isZero = func(v reflect.Value) bool {
// Avoid panics calling IsZero on nil pointer.
return v.IsNil() || v.Interface().(isZeroer).IsZero()
}
case t.Implements(isZeroerType):
finfo.isZero = func(v reflect.Value) bool {
return v.Interface().(isZeroer).IsZero()
}
case reflect.PointerTo(t).Implements(isZeroerType):
finfo.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()
}
}
}
}

Expand All @@ -160,7 +201,8 @@ func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, erro
if finfo.flags&fMode == fAny {
finfo.flags |= fElement
}
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
if (finfo.flags&fOmitEmpty != 0 || finfo.flags&fOmitZero != 0) &&
finfo.flags&(fElement|fAttr) == 0 {
valid = false
}
if !valid {
Expand Down