Skip to content

Commit c430438

Browse files
authored
Support omitzero (#729)
1 parent 500180b commit c430438

File tree

5 files changed

+243
-8
lines changed

5 files changed

+243
-8
lines changed

encode.go

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Encoder struct {
3838
anchorNameMap map[string]struct{}
3939
anchorCallback func(*ast.AnchorNode, interface{}) error
4040
customMarshalerMap map[reflect.Type]func(interface{}) ([]byte, error)
41+
omitZero bool
4142
omitEmpty bool
4243
autoInt bool
4344
useLiteralStyleIfMultiline bool
@@ -728,7 +729,7 @@ type IsZeroer interface {
728729
IsZero() bool
729730
}
730731

731-
func (e *Encoder) isZeroValue(v reflect.Value) bool {
732+
func (e *Encoder) isOmittedByOmitZero(v reflect.Value) bool {
732733
kind := v.Kind()
733734
if z, ok := v.Interface().(IsZeroer); ok {
734735
if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
@@ -737,13 +738,74 @@ func (e *Encoder) isZeroValue(v reflect.Value) bool {
737738
return z.IsZero()
738739
}
739740
switch kind {
741+
case reflect.String:
742+
return len(v.String()) == 0
743+
case reflect.Interface, reflect.Ptr, reflect.Slice, reflect.Map:
744+
return v.IsNil()
745+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
746+
return v.Int() == 0
747+
case reflect.Float32, reflect.Float64:
748+
return v.Float() == 0
749+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
750+
return v.Uint() == 0
751+
case reflect.Bool:
752+
return !v.Bool()
753+
case reflect.Struct:
754+
vt := v.Type()
755+
for i := v.NumField() - 1; i >= 0; i-- {
756+
if vt.Field(i).PkgPath != "" {
757+
continue // private field
758+
}
759+
if !e.isOmittedByOmitZero(v.Field(i)) {
760+
return false
761+
}
762+
}
763+
return true
764+
}
765+
return false
766+
}
767+
768+
func (e *Encoder) isOmittedByOmitEmptyOption(v reflect.Value) bool {
769+
switch v.Kind() {
740770
case reflect.String:
741771
return len(v.String()) == 0
742772
case reflect.Interface, reflect.Ptr:
743773
return v.IsNil()
744-
case reflect.Slice:
774+
case reflect.Slice, reflect.Map:
745775
return v.Len() == 0
746-
case reflect.Map:
776+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
777+
return v.Int() == 0
778+
case reflect.Float32, reflect.Float64:
779+
return v.Float() == 0
780+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
781+
return v.Uint() == 0
782+
case reflect.Bool:
783+
return !v.Bool()
784+
}
785+
return false
786+
}
787+
788+
// The current implementation of the omitempty tag combines the functionality of encoding/json's omitempty and omitzero tags.
789+
// This stems from a historical decision to respect the implementation of gopkg.in/yaml.v2, but it has caused confusion,
790+
// so we are working to integrate it into the functionality of encoding/json. (However, this will take some time.)
791+
// In the current implementation, in addition to the exclusion conditions of omitempty,
792+
// if a type implements IsZero, that implementation will be used.
793+
// Furthermore, for non-pointer structs, if all fields are eligible for exclusion,
794+
// the struct itself will also be excluded. These behaviors are originally the functionality of omitzero.
795+
func (e *Encoder) isOmittedByOmitEmptyTag(v reflect.Value) bool {
796+
kind := v.Kind()
797+
if z, ok := v.Interface().(IsZeroer); ok {
798+
if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
799+
return true
800+
}
801+
return z.IsZero()
802+
}
803+
switch kind {
804+
case reflect.String:
805+
return len(v.String()) == 0
806+
case reflect.Interface, reflect.Ptr:
807+
return v.IsNil()
808+
case reflect.Slice, reflect.Map:
747809
return v.Len() == 0
748810
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
749811
return v.Int() == 0
@@ -759,7 +821,7 @@ func (e *Encoder) isZeroValue(v reflect.Value) bool {
759821
if vt.Field(i).PkgPath != "" {
760822
continue // private field
761823
}
762-
if !e.isZeroValue(v.Field(i)) {
824+
if !e.isOmittedByOmitEmptyTag(v.Field(i)) {
763825
return false
764826
}
765827
}
@@ -818,8 +880,16 @@ func (e *Encoder) encodeStruct(ctx context.Context, value reflect.Value, column
818880
}
819881
fieldValue := value.FieldByName(field.Name)
820882
sf := fieldMap[field.Name]
821-
if (e.omitEmpty || sf.IsOmitEmpty) && e.isZeroValue(fieldValue) {
822-
// omit encoding
883+
if (e.omitZero || sf.IsOmitZero) && e.isOmittedByOmitZero(fieldValue) {
884+
// omit encoding by omitzero tag or OmitZero option.
885+
continue
886+
}
887+
if e.omitEmpty && e.isOmittedByOmitEmptyOption(fieldValue) {
888+
// omit encoding by OmitEmpty option.
889+
continue
890+
}
891+
if sf.IsOmitEmpty && e.isOmittedByOmitEmptyTag(fieldValue) {
892+
// omit encoding by omitempty tag.
823893
continue
824894
}
825895
ve := e

encode_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,111 @@ func TestEncoder(t *testing.T) {
584584
nil,
585585
},
586586

587+
// omitzero flag.
588+
{
589+
"a: 1\n",
590+
struct {
591+
A int `yaml:"a,omitzero"`
592+
B int `yaml:"b,omitzero"`
593+
}{1, 0},
594+
nil,
595+
},
596+
{
597+
"{}\n",
598+
struct {
599+
A int `yaml:"a,omitzero"`
600+
B int `yaml:"b,omitzero"`
601+
}{0, 0},
602+
nil,
603+
},
604+
{
605+
"a:\n \"y\": \"\"\n",
606+
struct {
607+
A *struct {
608+
X string `yaml:"x,omitzero"`
609+
Y string
610+
}
611+
}{&struct {
612+
X string `yaml:"x,omitzero"`
613+
Y string
614+
}{}},
615+
nil,
616+
},
617+
{
618+
"a: {}\n",
619+
struct {
620+
A *struct {
621+
X string `yaml:"x,omitzero"`
622+
Y string `yaml:"y,omitzero"`
623+
}
624+
}{&struct {
625+
X string `yaml:"x,omitzero"`
626+
Y string `yaml:"y,omitzero"`
627+
}{}},
628+
nil,
629+
},
630+
{
631+
"a: {x: 1}\n",
632+
struct {
633+
A *struct{ X, y int } `yaml:"a,omitzero,flow"`
634+
}{&struct{ X, y int }{1, 2}},
635+
nil,
636+
},
637+
{
638+
"{}\n",
639+
struct {
640+
A *struct{ X, y int } `yaml:"a,omitzero,flow"`
641+
}{nil},
642+
nil,
643+
},
644+
{
645+
"a: {x: 0}\n",
646+
struct {
647+
A *struct{ X, y int } `yaml:"a,omitzero,flow"`
648+
}{&struct{ X, y int }{}},
649+
nil,
650+
},
651+
{
652+
"a: {x: 1}\n",
653+
struct {
654+
A struct{ X, y int } `yaml:"a,omitzero,flow"`
655+
}{struct{ X, y int }{1, 2}},
656+
nil,
657+
},
658+
{
659+
"{}\n",
660+
struct {
661+
A struct{ X, y int } `yaml:"a,omitzero,flow"`
662+
}{struct{ X, y int }{0, 1}},
663+
nil,
664+
},
665+
{
666+
"a: 1.0\n",
667+
struct {
668+
A float64 `yaml:"a,omitzero"`
669+
B float64 `yaml:"b,omitzero"`
670+
}{1, 0},
671+
nil,
672+
},
673+
{
674+
"a: 1\nb: []\n",
675+
struct {
676+
A int
677+
B []string `yaml:"b,omitzero"`
678+
}{
679+
1, []string{},
680+
},
681+
nil,
682+
},
683+
{
684+
"{}\n",
685+
struct {
686+
A netip.Addr `yaml:"a,omitzero"`
687+
B struct{ X, y int } `yaml:"b,omitzero"`
688+
}{},
689+
nil,
690+
},
691+
587692
// OmitEmpty global option.
588693
{
589694
"a: 1\n",
@@ -605,6 +710,48 @@ func TestEncoder(t *testing.T) {
605710
yaml.OmitEmpty(),
606711
},
607712
},
713+
{
714+
"a: \"\"\nb: {}\n",
715+
struct {
716+
A netip.Addr `yaml:"a"`
717+
B struct{ X, y int } `yaml:"b"`
718+
}{},
719+
[]yaml.EncodeOption{
720+
yaml.OmitEmpty(),
721+
},
722+
},
723+
724+
// OmitZero global option.
725+
{
726+
"a: 1\n",
727+
struct {
728+
A int
729+
B int
730+
}{1, 0},
731+
[]yaml.EncodeOption{
732+
yaml.OmitZero(),
733+
},
734+
},
735+
{
736+
"{}\n",
737+
struct {
738+
A int
739+
B int
740+
}{0, 0},
741+
[]yaml.EncodeOption{
742+
yaml.OmitZero(),
743+
},
744+
},
745+
{
746+
"{}\n",
747+
struct {
748+
A netip.Addr `yaml:"a"`
749+
B struct{ X, y int } `yaml:"b"`
750+
}{},
751+
[]yaml.EncodeOption{
752+
yaml.OmitZero(),
753+
},
754+
},
608755

609756
// Flow flag.
610757
{

option.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,26 @@ func AutoInt() EncodeOption {
215215
}
216216
}
217217

218-
// OmitEmpty forces the encoder to assume an `omitempty` struct tag is
219-
// set on all the fields. See `Marshal` commentary for the `omitempty` tag logic.
218+
// OmitEmpty behaves in the same way as the interpretation of the omitempty tag in the encoding/json library.
219+
// set on all the fields.
220+
// In the current implementation, the omitempty tag is not implemented in the same way as encoding/json,
221+
// so please specify this option if you expect the same behavior.
220222
func OmitEmpty() EncodeOption {
221223
return func(e *Encoder) error {
222224
e.omitEmpty = true
223225
return nil
224226
}
225227
}
226228

229+
// OmitZero forces the encoder to assume an `omitzero` struct tag is
230+
// set on all the fields. See `Marshal` commentary for the `omitzero` tag logic.
231+
func OmitZero() EncodeOption {
232+
return func(e *Encoder) error {
233+
e.omitZero = true
234+
return nil
235+
}
236+
}
237+
227238
// CommentPosition type of the position for comment.
228239
type CommentPosition int
229240

struct.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type StructField struct {
2020
IsAutoAnchor bool
2121
IsAutoAlias bool
2222
IsOmitEmpty bool
23+
IsOmitZero bool
2324
IsFlow bool
2425
IsInline bool
2526
}
@@ -53,6 +54,8 @@ func structField(field reflect.StructField) *StructField {
5354
switch {
5455
case opt == "omitempty":
5556
sf.IsOmitEmpty = true
57+
case opt == "omitzero":
58+
sf.IsOmitZero = true
5659
case opt == "flow":
5760
sf.IsFlow = true
5861
case opt == "inline":

yaml.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ func (s MapSlice) ToMap() map[interface{}]interface{} {
112112
// encoding/json 'omitempty' definition. It combines some elements
113113
// of 'omitempty' and 'omitzero'. See https://github.com/goccy/go-yaml/issues/695.
114114
//
115+
// omitzero The omitzero tag behaves in the same way as the interpretation of the omitzero tag in the encoding/json library.
116+
// 1) If the field type has an "IsZero() bool" method, that will be used to determine whether the value is zero.
117+
// 2) Otherwise, the value is zero if it is the zero value for its type.
118+
//
115119
// flow Marshal using a flow style (useful for structs,
116120
// sequences and maps).
117121
//

0 commit comments

Comments
 (0)