Repository: goccy/go-yaml
Pull request #729 (2025-05-09 04:34:50)

Support omitzero
fix https://github.com/goccy/go-yaml/issues/695

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/goccy/go-yaml/pull/729?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
Attention: Patch coverage is `73.43750%` with `17 lines` in your changes missing coverage. Please review.
> Project coverage is 77.83%. Comparing base [(`500180b`)](https://app.codecov.io/gh/goccy/go-yaml/commit/500180b7b72264c5442e1d5e32269f73f89eb28c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`ea34559`)](https://app.codecov.io/gh/goccy/go-yaml/commit/ea34559cb53c2f79436c8d79fa4b0bee303c3b63?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima).

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #729      +/-   ##
==========================================
- Coverage   77.94%   77.83%   -0.12%     
==========================================
  Files          22       22              
  Lines        7998     8056      +58     
==========================================
+ Hits         6234     6270      +36     
- Misses       1349     1370      +21     
- Partials      415      416       +1     
```

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
goccy: @bwplotka 

After discussing with co-maintainer @shuheiktgw , we have decided to merge this PR as is.

The reasons are as follows:

### Prerequisites

- The behavior of `omitempty` and `omitzero` in goccy/go-yaml should align with `encoding/json` .
- However, currently, the behavior of the `omitempty` tag matches the behavior when both `omitempty` and `omitzero` are specified in `encoding/json`.
- While we intend to fix the current behavior of the `omitempty` tag in the future, we plan to leave it as is for now and will not change it arbitrarily.
- `OmitEmpty` and `OmitZero` options are being added for the first time in this release. In other words, no one has used them yet.

### Reasons for the Decision

For users newly utilizing the `OmitEmpty` option, the fact that its behavior differs from the current `omitempty` tag is not considered a significant issue. This is because it is important for new users of this option to read its documentation and use it correctly. Even if they do not read it, they will notice the difference during development since the behavior differs from the current `omitempty` tag in goccy/go-yaml.
Additionally, if they expect the behavior of the `omitempty` tag in goccy/go-yaml, they can meet their requirements by specifying both the `OmitEmpty` and `OmitZero` options.

If we were to align the behavior of the `omitempty` tag in goccy/go-yaml with the `OmitEmpty` option, and if we wanted to align the behavior with `encoding/json` in the future, we would need to change the behavior of both the tag and option. We do not want to bear this adjustment cost. We want to focus solely on the tag.

Patch:
diff --git a/encode.go b/encode.go
index 0a789bf..73bc808 100644
--- a/encode.go
+++ b/encode.go
@@ -38,6 +38,7 @@ type Encoder struct {
 	anchorNameMap              map[string]struct{}
 	anchorCallback             func(*ast.AnchorNode, interface{}) error
 	customMarshalerMap         map[reflect.Type]func(interface{}) ([]byte, error)
+	omitZero                   bool
 	omitEmpty                  bool
 	autoInt                    bool
 	useLiteralStyleIfMultiline bool
@@ -728,7 +729,7 @@ type IsZeroer interface {
 	IsZero() bool
 }
 
-func (e *Encoder) isZeroValue(v reflect.Value) bool {
+func (e *Encoder) isOmittedByOmitZero(v reflect.Value) bool {
 	kind := v.Kind()
 	if z, ok := v.Interface().(IsZeroer); ok {
 		if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
@@ -737,13 +738,74 @@ func (e *Encoder) isZeroValue(v reflect.Value) bool {
 		return z.IsZero()
 	}
 	switch kind {
+	case reflect.String:
+		return len(v.String()) == 0
+	case reflect.Interface, reflect.Ptr, reflect.Slice, reflect.Map:
+		return v.IsNil()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Struct:
+		vt := v.Type()
+		for i := v.NumField() - 1; i >= 0; i-- {
+			if vt.Field(i).PkgPath != "" {
+				continue // private field
+			}
+			if !e.isOmittedByOmitZero(v.Field(i)) {
+				return false
+			}
+		}
+		return true
+	}
+	return false
+}
+
+func (e *Encoder) isOmittedByOmitEmptyOption(v reflect.Value) bool {
+	switch v.Kind() {
 	case reflect.String:
 		return len(v.String()) == 0
 	case reflect.Interface, reflect.Ptr:
 		return v.IsNil()
-	case reflect.Slice:
+	case reflect.Slice, reflect.Map:
 		return v.Len() == 0
-	case reflect.Map:
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	}
+	return false
+}
+
+// The current implementation of the omitempty tag combines the functionality of encoding/json's omitempty and omitzero tags.
+// This stems from a historical decision to respect the implementation of gopkg.in/yaml.v2, but it has caused confusion,
+// so we are working to integrate it into the functionality of encoding/json. (However, this will take some time.)
+// In the current implementation, in addition to the exclusion conditions of omitempty,
+// if a type implements IsZero, that implementation will be used.
+// Furthermore, for non-pointer structs, if all fields are eligible for exclusion,
+// the struct itself will also be excluded. These behaviors are originally the functionality of omitzero.
+func (e *Encoder) isOmittedByOmitEmptyTag(v reflect.Value) bool {
+	kind := v.Kind()
+	if z, ok := v.Interface().(IsZeroer); ok {
+		if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
+			return true
+		}
+		return z.IsZero()
+	}
+	switch kind {
+	case reflect.String:
+		return len(v.String()) == 0
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	case reflect.Slice, reflect.Map:
 		return v.Len() == 0
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		return v.Int() == 0
@@ -759,7 +821,7 @@ func (e *Encoder) isZeroValue(v reflect.Value) bool {
 			if vt.Field(i).PkgPath != "" {
 				continue // private field
 			}
-			if !e.isZeroValue(v.Field(i)) {
+			if !e.isOmittedByOmitEmptyTag(v.Field(i)) {
 				return false
 			}
 		}
@@ -818,8 +880,16 @@ func (e *Encoder) encodeStruct(ctx context.Context, value reflect.Value, column
 		}
 		fieldValue := value.FieldByName(field.Name)
 		sf := fieldMap[field.Name]
-		if (e.omitEmpty || sf.IsOmitEmpty) && e.isZeroValue(fieldValue) {
-			// omit encoding
+		if (e.omitZero || sf.IsOmitZero) && e.isOmittedByOmitZero(fieldValue) {
+			// omit encoding by omitzero tag or OmitZero option.
+			continue
+		}
+		if e.omitEmpty && e.isOmittedByOmitEmptyOption(fieldValue) {
+			// omit encoding by OmitEmpty option.
+			continue
+		}
+		if sf.IsOmitEmpty && e.isOmittedByOmitEmptyTag(fieldValue) {
+			// omit encoding by omitempty tag.
 			continue
 		}
 		ve := e
diff --git a/option.go b/option.go
index 3849249..12927af 100644
--- a/option.go
+++ b/option.go
@@ -215,8 +215,10 @@ func AutoInt() EncodeOption {
 	}
 }
 
-// OmitEmpty forces the encoder to assume an `omitempty` struct tag is
-// set on all the fields. See `Marshal` commentary for the `omitempty` tag logic.
+// OmitEmpty behaves in the same way as the interpretation of the omitempty tag in the encoding/json library.
+// set on all the fields.
+// In the current implementation, the omitempty tag is not implemented in the same way as encoding/json,
+// so please specify this option if you expect the same behavior.
 func OmitEmpty() EncodeOption {
 	return func(e *Encoder) error {
 		e.omitEmpty = true
@@ -224,6 +226,15 @@ func OmitEmpty() EncodeOption {
 	}
 }
 
+// OmitZero forces the encoder to assume an `omitzero` struct tag is
+// set on all the fields. See `Marshal` commentary for the `omitzero` tag logic.
+func OmitZero() EncodeOption {
+	return func(e *Encoder) error {
+		e.omitZero = true
+		return nil
+	}
+}
+
 // CommentPosition type of the position for comment.
 type CommentPosition int
 
diff --git a/struct.go b/struct.go
index 884924b..ece77f5 100644
--- a/struct.go
+++ b/struct.go
@@ -20,6 +20,7 @@ type StructField struct {
 	IsAutoAnchor bool
 	IsAutoAlias  bool
 	IsOmitEmpty  bool
+	IsOmitZero   bool
 	IsFlow       bool
 	IsInline     bool
 }
@@ -53,6 +54,8 @@ func structField(field reflect.StructField) *StructField {
 			switch {
 			case opt == "omitempty":
 				sf.IsOmitEmpty = true
+			case opt == "omitzero":
+				sf.IsOmitZero = true
 			case opt == "flow":
 				sf.IsFlow = true
 			case opt == "inline":
diff --git a/yaml.go b/yaml.go
index b617fa4..161f3e0 100644
--- a/yaml.go
+++ b/yaml.go
@@ -112,6 +112,10 @@ func (s MapSlice) ToMap() map[interface{}]interface{} {
 //	             encoding/json 'omitempty' definition. It combines some elements
 //	             of 'omitempty' and 'omitzero'. See https://github.com/goccy/go-yaml/issues/695.
 //
+//	omitzero      The omitzero tag behaves in the same way as the interpretation of the omitzero tag in the encoding/json library.
+//	              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.
+//
 //	flow         Marshal using a flow style (useful for structs,
 //	             sequences and maps).
 //


Test patch:
diff --git a/encode_test.go b/encode_test.go
index 613931c..49a2a7f 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -584,6 +584,111 @@ func TestEncoder(t *testing.T) {
 			nil,
 		},
 
+		// omitzero flag.
+		{
+			"a: 1\n",
+			struct {
+				A int `yaml:"a,omitzero"`
+				B int `yaml:"b,omitzero"`
+			}{1, 0},
+			nil,
+		},
+		{
+			"{}\n",
+			struct {
+				A int `yaml:"a,omitzero"`
+				B int `yaml:"b,omitzero"`
+			}{0, 0},
+			nil,
+		},
+		{
+			"a:\n  \"y\": \"\"\n",
+			struct {
+				A *struct {
+					X string `yaml:"x,omitzero"`
+					Y string
+				}
+			}{&struct {
+				X string `yaml:"x,omitzero"`
+				Y string
+			}{}},
+			nil,
+		},
+		{
+			"a: {}\n",
+			struct {
+				A *struct {
+					X string `yaml:"x,omitzero"`
+					Y string `yaml:"y,omitzero"`
+				}
+			}{&struct {
+				X string `yaml:"x,omitzero"`
+				Y string `yaml:"y,omitzero"`
+			}{}},
+			nil,
+		},
+		{
+			"a: {x: 1}\n",
+			struct {
+				A *struct{ X, y int } `yaml:"a,omitzero,flow"`
+			}{&struct{ X, y int }{1, 2}},
+			nil,
+		},
+		{
+			"{}\n",
+			struct {
+				A *struct{ X, y int } `yaml:"a,omitzero,flow"`
+			}{nil},
+			nil,
+		},
+		{
+			"a: {x: 0}\n",
+			struct {
+				A *struct{ X, y int } `yaml:"a,omitzero,flow"`
+			}{&struct{ X, y int }{}},
+			nil,
+		},
+		{
+			"a: {x: 1}\n",
+			struct {
+				A struct{ X, y int } `yaml:"a,omitzero,flow"`
+			}{struct{ X, y int }{1, 2}},
+			nil,
+		},
+		{
+			"{}\n",
+			struct {
+				A struct{ X, y int } `yaml:"a,omitzero,flow"`
+			}{struct{ X, y int }{0, 1}},
+			nil,
+		},
+		{
+			"a: 1.0\n",
+			struct {
+				A float64 `yaml:"a,omitzero"`
+				B float64 `yaml:"b,omitzero"`
+			}{1, 0},
+			nil,
+		},
+		{
+			"a: 1\nb: []\n",
+			struct {
+				A int
+				B []string `yaml:"b,omitzero"`
+			}{
+				1, []string{},
+			},
+			nil,
+		},
+		{
+			"{}\n",
+			struct {
+				A netip.Addr         `yaml:"a,omitzero"`
+				B struct{ X, y int } `yaml:"b,omitzero"`
+			}{},
+			nil,
+		},
+
 		// OmitEmpty global option.
 		{
 			"a: 1\n",
@@ -605,6 +710,48 @@ func TestEncoder(t *testing.T) {
 				yaml.OmitEmpty(),
 			},
 		},
+		{
+			"a: \"\"\nb: {}\n",
+			struct {
+				A netip.Addr         `yaml:"a"`
+				B struct{ X, y int } `yaml:"b"`
+			}{},
+			[]yaml.EncodeOption{
+				yaml.OmitEmpty(),
+			},
+		},
+
+		// OmitZero global option.
+		{
+			"a: 1\n",
+			struct {
+				A int
+				B int
+			}{1, 0},
+			[]yaml.EncodeOption{
+				yaml.OmitZero(),
+			},
+		},
+		{
+			"{}\n",
+			struct {
+				A int
+				B int
+			}{0, 0},
+			[]yaml.EncodeOption{
+				yaml.OmitZero(),
+			},
+		},
+		{
+			"{}\n",
+			struct {
+				A netip.Addr         `yaml:"a"`
+				B struct{ X, y int } `yaml:"b"`
+			}{},
+			[]yaml.EncodeOption{
+				yaml.OmitZero(),
+			},
+		},
 
 		// Flow flag.
 		{