Skip to content

Commit

Permalink
feat: Add support for omitting empty and zero values in validation (i…
Browse files Browse the repository at this point in the history
…ncluding nil pointer and empty content of pointer) (#1289)

## Fixes Or Enhances

**Add the `omitzero` Tag:**
- It allows you to ignore the subsequent validations if the value of a
field is empty
- If the field is a pointer and the pointer is nil, ignore the
subsequent validations
- If the field is a pointer and the pointer is not nil, but the content
value of the pointer is zero, the subsequent validations will be ignored


**Make sure that you've checked the boxes below before you submit PR:**
- [x] Tests exist or have been written that cover this particular
change.

@go-playground/validator-maintainers
  • Loading branch information
zeewell authored Feb 15, 2025
1 parent c171f2d commit f5f02dc
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 0 deletions.
15 changes: 15 additions & 0 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ var (
keysTag: {},
endKeysTag: {},
structOnlyTag: {},
omitzero: {},
omitempty: {},
omitnil: {},
skipValidationTag: {},
Expand Down Expand Up @@ -1797,6 +1798,20 @@ func hasValue(fl FieldLevel) bool {
}
}

// hasNotZeroValue is the validation function for validating if the current field's value is not the zero value for its type.
func hasNotZeroValue(fl FieldLevel) bool {
field := fl.Field()
switch field.Kind() {
case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
return !field.IsNil()
default:
if fl.(*validate).fldIsPointer && field.Interface() != nil {
return !field.IsZero()
}
return field.IsValid() && !field.IsZero()
}
}

// requireCheckFieldKind is a func for check field kind
func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue bool) bool {
field := fl.Field()
Expand Down
5 changes: 5 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
typeKeys
typeEndKeys
typeOmitNil
typeOmitZero
)

const (
Expand Down Expand Up @@ -249,6 +250,10 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
}
return

case omitzero:
current.typeof = typeOmitZero
continue

case omitempty:
current.typeof = typeOmitEmpty
continue
Expand Down
17 changes: 17 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
return
}

if ct.typeof == typeOmitZero {
return
}

if ct.hasTag {
if kind == reflect.Invalid {
v.str1 = string(append(ns, cf.altName...))
Expand Down Expand Up @@ -238,6 +242,19 @@ OUTER:
ct = ct.next
continue

case typeOmitZero:
v.slflParent = parent
v.flField = current
v.cf = cf
v.ct = ct

if !hasNotZeroValue(v) {
return
}

ct = ct.next
continue

case typeOmitNil:
v.slflParent = parent
v.flField = current
Expand Down
1 change: 1 addition & 0 deletions validator_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
tagKeySeparator = "="
structOnlyTag = "structonly"
noStructLevelTag = "nostructlevel"
omitzero = "omitzero"
omitempty = "omitempty"
omitnil = "omitnil"
isdefault = "isdefault"
Expand Down
51 changes: 51 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14022,6 +14022,57 @@ func TestOmitNilAndRequired(t *testing.T) {
})
}

func TestOmitZero(t *testing.T) {
type (
OmitEmpty struct {
Str string `validate:"omitempty,min=10"`
StrPtr *string `validate:"omitempty,min=10"`
}
OmitZero struct {
Str string `validate:"omitzero,min=10"`
StrPtr *string `validate:"omitzero,min=10"`
}
)

var (
validate = New()
valid = "this is the long string to pass the validation rule"
empty = ""
)

t.Run("compare using valid data", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{Str: valid, StrPtr: &valid})
err2 := validate.Struct(OmitZero{Str: valid, StrPtr: &valid})

Equal(t, err1, nil)
Equal(t, err2, nil)
})

t.Run("compare fully empty omitempty and omitzero", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{})
err2 := validate.Struct(OmitZero{})

Equal(t, err1, nil)
Equal(t, err2, nil)
})

t.Run("compare with zero value", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{Str: "", StrPtr: nil})
err2 := validate.Struct(OmitZero{Str: "", StrPtr: nil})

Equal(t, err1, nil)
Equal(t, err2, nil)
})

t.Run("compare with empty value", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{Str: empty, StrPtr: &empty})
err2 := validate.Struct(OmitZero{Str: empty, StrPtr: &empty})

AssertError(t, err1, "OmitEmpty.StrPtr", "OmitEmpty.StrPtr", "StrPtr", "StrPtr", "min")
Equal(t, err2, nil)
})
}

func TestPrivateFieldsStruct(t *testing.T) {
type tc struct {
stct interface{}
Expand Down

0 comments on commit f5f02dc

Please sign in to comment.