Skip to content

Add support for required and nonzero yaml fields #728

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 3 commits 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
13 changes: 13 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,11 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N
}
v, exists := keyToNodeMap[structField.RenderName]
if !exists {
if structField.IsRequired && foundErr == nil {
foundErr = errors.ErrRequiredField(fmt.Sprintf("%s.%s", structType.Name(), field.Name), src.GetToken())
} else if structField.IsNonEmpty && foundErr == nil {
foundErr = errors.ErrEmptyField(fmt.Sprintf("%s.%s", structType.Name(), field.Name), src.GetToken())
}
continue
}
delete(unknownFields, structField.RenderName)
Expand All @@ -1436,6 +1441,14 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N
}
continue
}
if structField.IsNonZero && isZero(newFieldValue) && foundErr == nil {
foundErr = errors.ErrZeroField(fmt.Sprintf("%s.%s", structType.Name(), field.Name), src.GetToken())
continue
}
if structField.IsNonEmpty && isEmptyForTag(newFieldValue) && foundErr == nil {
foundErr = errors.ErrEmptyField(fmt.Sprintf("%s.%s", structType.Name(), field.Name), src.GetToken())
continue
}
fieldValue.Set(newFieldValue)
}
if foundErr != nil {
Expand Down
158 changes: 156 additions & 2 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2971,8 +2971,8 @@ func TestDecoder_LiteralWithNewLine(t *testing.T) {

func TestDecoder_TabCharacterAtRight(t *testing.T) {
yml := `
- a: [2 , 2]
b: [2 , 2]
- a: [2 , 2]
b: [2 , 2]
c: [2 , 2]`
var v []map[string][]int
if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
Expand Down Expand Up @@ -3769,3 +3769,157 @@ func TestSetNullValue(t *testing.T) {
})
}
}

func TestRequiredFieldDecode(t *testing.T) {
yml := `a: 0`
var v struct {
A int `yaml:"a,required"`
}
if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
t.Fatal(err)
}

yml = `{}`
err := yaml.Unmarshal([]byte(yml), &v)
if err == nil {
t.Fatalf("expect error, but got nil")
}
msg := err.Error()
if !strings.Contains(msg, "required field .A is missing") {
t.Fatalf("expect error message to contain %q, but got %q", "required field .A is missing", msg)
}
}

func TestNonEmptyFieldDecode(t *testing.T) {
yml := `s: "hello"`
var v struct {
S string `yaml:"s,nonempty"`
}
if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
t.Fatal(err)
}

yml = `s: ""`
err := yaml.Unmarshal([]byte(yml), &v)
if _, ok := err.(*errors.EmptyFieldError); !ok {
t.Fatalf("expect EmptyFieldError, but got %v", err)
}

// This is a nonempty list with an empty value of its element type, but the list itself is nonempty
yml = `a: [0]`
var w struct {
A []int `yaml:"a,nonempty"`
}
if err := yaml.Unmarshal([]byte(yml), &w); err != nil {
t.Fatal(err)
}

yml = `a: []`
err = yaml.Unmarshal([]byte(yml), &w)
if _, ok := err.(*errors.EmptyFieldError); !ok {
t.Fatalf("expect EmptyFieldError, but got %v", err)
}

// The yaml object is here but the field is not set and therefore is empty
yml = `{}`
err = yaml.Unmarshal([]byte(yml), &w)
if _, ok := err.(*errors.EmptyFieldError); !ok {
t.Fatalf("expect EmptyFieldError, but got %v", err)
}
}

func TestRequiredAndNonEmptyFieldDecode(t *testing.T) {
yml := `a: "existing"`
var v struct {
A string `yaml:"a,required,nonempty"`
}
if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
t.Fatal(err)
}

yml = `a: ""`
err := yaml.Unmarshal([]byte(yml), &v)
if _, ok := err.(*errors.EmptyFieldError); !ok {
t.Fatalf("expect EmptyFieldError, but got %v", err)
}

yml = `{}` // empty object is missing `a` entirely but `a` is required
err = yaml.Unmarshal([]byte(yml), &v)
if _, ok := err.(*errors.RequiredFieldError); !ok {
t.Fatalf("expect RequiredFieldError, but got %v", err)
}
}

func TestNonZeroFieldDecode(t *testing.T) {
yml := `s: "hello"`
var v struct {
S string `yaml:"s,nonzero"`
}
if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
t.Fatal(err)
}

yml = `s: ""`
err := yaml.Unmarshal([]byte(yml), &v)
if _, ok := err.(*errors.ZeroFieldError); !ok {
t.Fatalf("expect ZeroFieldError, but got %v", err)
}

yml = `a: [0]`
var w struct {
A []int `yaml:"a,nonzero"`
}
if err := yaml.Unmarshal([]byte(yml), &w); err != nil {
t.Fatal(err)
}

yml = `a: []`
if err = yaml.Unmarshal([]byte(yml), &w); err != nil {
t.Fatalf("expect no error, but got %v", err)
}

yml = `{}`
if err := yaml.Unmarshal([]byte(yml), &w); err != nil {
t.Fatalf("expect no error, but got %v", err)
}
}

func TestNonemptyAndNonzeroFieldDecode(t *testing.T) {
yml := `s: "hello"`
var v struct {
S string `yaml:"s,nonempty,nonzero"`
}
if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
t.Fatal(err)
}

// A value which is zero will always also be empty, we prefer the zero error to distinguish the two cases
yml = `s: ""`
err := yaml.Unmarshal([]byte(yml), &v)
if _, ok := err.(*errors.ZeroFieldError); !ok {
t.Fatalf("expect ZeroFieldError, but got %v", err)
}

// This is a nonempty list with a zero value of its element type, but the list itself is nonempty and nonzero
yml = `a: [0]`
var w struct {
A []int `yaml:"a,nonempty,nonzero"`
}
if err := yaml.Unmarshal([]byte(yml), &w); err != nil {
t.Fatal(err)
}

// The yaml array is here and is empty, therefore it is empty but not zero
yml = `a: []`
err = yaml.Unmarshal([]byte(yml), &w)
if _, ok := err.(*errors.EmptyFieldError); !ok {
t.Fatalf("expect EmptyFieldError, but got %v", err)
}

// The yaml object is here but the field is not set and therefore it is empty but not zero
yml = `{}`
err = yaml.Unmarshal([]byte(yml), &w)
if _, ok := err.(*errors.EmptyFieldError); !ok {
t.Fatalf("expect EmptyFieldError, but got %v", err)
}
}
111 changes: 111 additions & 0 deletions empty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package yaml

import "reflect"

// IsZeroer is used to check whether an object is zero to determine
// whether it should be omitted when marshaling with the omitempty flag.
// One notable implementation is time.Time.
type IsZeroer interface {
IsZero() bool
}

func isZero(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, 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 !isZero(v.Field(i)) {
return false
}
}
return true
}
return false
}

func isEmptyForOption(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, reflect.Map:
return v.Len() == 0
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 isEmptyForTag(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
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 !isEmptyForTag(v.Field(i)) {
return false
}
}
return true
}
return false
}
Loading