Repository: goccy/go-yaml
Pull request #737 (2025-05-17 09:37:17)

feat: Dont make copies of structs for validation
Following [this comment](https://github.com/goccy/go-yaml/pull/665#pullrequestreview-2830568466), I took over the original PR and updated it.

Before submitting your PR, please confirm the following.

- [x] Describe the purpose for which you created this PR.  
- [x] Create test code that corresponds to the modification

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/737?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 77.94%. 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 [(`b6dea53`)](https://app.codecov.io/gh/goccy/go-yaml/commit/b6dea53c6b11947b106c90b66c1b028a0d304b91?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     #737   +/-   ##
=======================================
  Coverage   77.94%   77.94%           
=======================================
  Files          22       22           
  Lines        7998     7998           
=======================================
  Hits         6234     6234           
  Misses       1349     1349           
  Partials      415      415           
```

</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>
asmfreak: Thanks! You can close my original PR

Patch:
diff --git a/decode.go b/decode.go
index 9c168fe..15fcba2 100644
--- a/decode.go
+++ b/decode.go
@@ -1442,7 +1442,7 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N
 	}
 
 	if d.validator != nil {
-		if err := d.validator.Struct(dst.Interface()); err != nil {
+		if err := d.validator.Struct(dst.Addr().Interface()); err != nil {
 			ev := reflect.ValueOf(err)
 			if ev.Type().Kind() == reflect.Slice {
 				for i := 0; i < ev.Len(); i++ {
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.
 		{