Repository: go-playground/validator
Pull request #1133 (2023-07-28 00:57:42)

Fix filepath validator panic if the value is an existing dir
## Fixes Or Enhances
This PR adds an additional check for directories in the isFilePath() function.

Connected to #1102 

**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

coveralls: 
[![Coverage Status](https://coveralls.io/builds/61630875/badge)](https://coveralls.io/builds/61630875)

coverage: 73.962% (-0.01%) from 73.972% when pulling **dcd21e81522381cfdf49e911f5fe8c398a9f551e on nodivbyzero:validator-file-path-fix-is-dir** into **bd1113d5c1901c5205fc0a7af031a11268ff13ee on go-playground:master**.


Patch:
diff --git a/README.md b/README.md
index 520661d..7cc0116 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 Package validator
 =================
-<img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v10/logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+<img align="right" src="logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 ![Project status](https://img.shields.io/badge/version-10.14.1-green.svg)
 [![Build Status](https://travis-ci.org/go-playground/validator.svg?branch=master)](https://travis-ci.org/go-playground/validator)
 [![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-playground/validator?branch=master)
diff --git a/_examples/simple/main.go b/_examples/simple/main.go
index 0d3ca51..503669e 100644
--- a/_examples/simple/main.go
+++ b/_examples/simple/main.go
@@ -12,6 +12,7 @@ type User struct {
 	LastName       string     `validate:"required"`
 	Age            uint8      `validate:"gte=0,lte=130"`
 	Email          string     `validate:"required,email"`
+	Gender         string     `validate:"oneof=male female prefer_not_to`
 	FavouriteColor string     `validate:"iscolor"`                // alias for 'hexcolor|rgb|rgba|hsl|hsla'
 	Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
 }
@@ -47,6 +48,7 @@ func validateStruct() {
 		FirstName:      "Badger",
 		LastName:       "Smith",
 		Age:            135,
+		Gender:         "male",
 		Email:          "Badger.Smith@gmail.com",
 		FavouriteColor: "#000-",
 		Addresses:      []*Address{address},
diff --git a/baked_in.go b/baked_in.go
index e676f1d..a0750cc 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -1561,6 +1561,10 @@ func isFilePath(fl FieldLevel) bool {
 
 	field := fl.Field()
 
+	// Not valid if it is a directory.
+	if isDir(fl) {
+		return false
+	}
 	// If it exists, it obviously is valid.
 	// This is done first to avoid code duplication and unnecessary additional logic.
 	if exists = isFile(fl); exists {
@@ -1710,7 +1714,7 @@ func hasValue(fl FieldLevel) bool {
 		if fl.(*validate).fldIsPointer && field.Interface() != nil {
 			return true
 		}
-		return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
+		return field.IsValid() && !field.IsZero()
 	}
 }
 
@@ -1734,7 +1738,7 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
 		if nullable && field.Interface() != nil {
 			return false
 		}
-		return field.IsValid() && field.Interface() == reflect.Zero(field.Type()).Interface()
+		return field.IsValid() && field.IsZero()
 	}
 }
 
diff --git a/cache.go b/cache.go
index bbfd2a4..ddd37b8 100644
--- a/cache.go
+++ b/cache.go
@@ -20,6 +20,7 @@ const (
 	typeOr
 	typeKeys
 	typeEndKeys
+	typeNestedStructLevel
 )
 
 const (
@@ -152,7 +153,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
 		// and so only struct level caching can be used instead of combined with Field tag caching
 
 		if len(tag) > 0 {
-			ctag, _ = v.parseFieldTagsRecursive(tag, fld.Name, "", false)
+			ctag, _ = v.parseFieldTagsRecursive(tag, fld, "", false)
 		} else {
 			// even if field doesn't have validations need cTag for traversing to potential inner/nested
 			// elements of the field.
@@ -171,7 +172,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
 	return cs
 }
 
-func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
+func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
 	var t string
 	noAlias := len(alias) == 0
 	tags := strings.Split(tag, tagSeparator)
@@ -185,9 +186,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
 		// check map for alias and process new tags, otherwise process as usual
 		if tagsVal, found := v.aliases[t]; found {
 			if i == 0 {
-				firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
+				firstCtag, current = v.parseFieldTagsRecursive(tagsVal, field, t, true)
 			} else {
-				next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
+				next, curr := v.parseFieldTagsRecursive(tagsVal, field, t, true)
 				current.next, current = next, curr
 
 			}
@@ -235,7 +236,7 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
 				}
 			}
 
-			current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false)
+			current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), field, "", false)
 			continue
 
 		case endKeysTag:
@@ -284,14 +285,18 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
 
 				current.tag = vals[0]
 				if len(current.tag) == 0 {
-					panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
+					panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, field.Name)))
 				}
 
 				if wrapper, ok := v.validations[current.tag]; ok {
 					current.fn = wrapper.fn
 					current.runValidationWhenNil = wrapper.runValidatinOnNil
 				} else {
-					panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
+					panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, field.Name)))
+				}
+
+				if current.typeof == typeDefault && isNestedStructOrStructPtr(field) {
+					current.typeof = typeNestedStructLevel
 				}
 
 				if len(orVals) > 1 {
@@ -319,7 +324,7 @@ func (v *Validate) fetchCacheTag(tag string) *cTag {
 		// isn't parsed again.
 		ctag, found = v.tagCache.Get(tag)
 		if !found {
-			ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
+			ctag, _ = v.parseFieldTagsRecursive(tag, reflect.StructField{}, "", false)
 			v.tagCache.Set(tag, ctag)
 		}
 	}
diff --git a/doc.go b/doc.go
index f5aa9e5..84467dc 100644
--- a/doc.go
+++ b/doc.go
@@ -247,7 +247,7 @@ Example #2
 This validates that the value is not the data types default zero value.
 For numbers ensures value is not zero. For strings ensures value is
 not "". For slices, maps, pointers, interfaces, channels and functions
-ensures the value is not nil.
+ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required
 
@@ -256,7 +256,7 @@ ensures the value is not nil.
 The field under validation must be present and not empty only if all
 the other specified fields are equal to the value following the specified
 field. For strings ensures value is not "". For slices, maps, pointers,
-interfaces, channels and functions ensures the value is not nil.
+interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required_if
 
@@ -273,7 +273,7 @@ Examples:
 The field under validation must be present and not empty unless all
 the other specified fields are equal to the value following the specified
 field. For strings ensures value is not "". For slices, maps, pointers,
-interfaces, channels and functions ensures the value is not nil.
+interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required_unless
 
@@ -290,7 +290,7 @@ Examples:
 The field under validation must be present and not empty only if any
 of the other specified fields are present. For strings ensures value is
 not "". For slices, maps, pointers, interfaces, channels and functions
-ensures the value is not nil.
+ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required_with
 
@@ -307,7 +307,7 @@ Examples:
 The field under validation must be present and not empty only if all
 of the other specified fields are present. For strings ensures value is
 not "". For slices, maps, pointers, interfaces, channels and functions
-ensures the value is not nil.
+ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required_with_all
 
@@ -321,7 +321,7 @@ Example:
 The field under validation must be present and not empty only when any
 of the other specified fields are not present. For strings ensures value is
 not "". For slices, maps, pointers, interfaces, channels and functions
-ensures the value is not nil.
+ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required_without
 
@@ -338,7 +338,7 @@ Examples:
 The field under validation must be present and not empty only when all
 of the other specified fields are not present. For strings ensures value is
 not "". For slices, maps, pointers, interfaces, channels and functions
-ensures the value is not nil.
+ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: required_without_all
 
@@ -352,7 +352,7 @@ Example:
 The field under validation must not be present or not empty only if all
 the other specified fields are equal to the value following the specified
 field. For strings ensures value is not "". For slices, maps, pointers,
-interfaces, channels and functions ensures the value is not nil.
+interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: excluded_if
 
@@ -369,7 +369,7 @@ Examples:
 The field under validation must not be present or empty unless all
 the other specified fields are equal to the value following the specified
 field. For strings ensures value is not "". For slices, maps, pointers,
-interfaces, channels and functions ensures the value is not nil.
+interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
 
 	Usage: excluded_unless
 
diff --git a/util.go b/util.go
index 3925cfe..b81d954 100644
--- a/util.go
+++ b/util.go
@@ -286,3 +286,11 @@ func panicIf(err error) {
 		panic(err.Error())
 	}
 }
+
+func isNestedStructOrStructPtr(v reflect.StructField) bool {
+	if v.Type == nil {
+		return false
+	}
+	kind := v.Type.Kind()
+	return kind == reflect.Struct || kind == reflect.Ptr && v.Type.Elem().Kind() == reflect.Struct
+}
diff --git a/validator.go b/validator.go
index 6f6d53a..a6fa1f5 100644
--- a/validator.go
+++ b/validator.go
@@ -170,7 +170,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
 
 				if ct.typeof == typeStructOnly {
 					goto CONTINUE
-				} else if ct.typeof == typeIsDefault {
+				} else if ct.typeof == typeIsDefault || ct.typeof == typeNestedStructLevel {
 					// set Field Level fields
 					v.slflParent = parent
 					v.flField = current


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 74f4945..97a50f6 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -5852,6 +5852,7 @@ func TestFilePathValidation(t *testing.T) {
 		{"valid filepath", filepath.Join("testdata", "a.go"), true},
 		{"invalid filepath", filepath.Join("testdata", "no\000.go"), false},
 		{"directory, not a filepath", "testdata" + string(os.PathSeparator), false},
+		{"directory", "testdata", false},
 	}
 
 	for _, test := range tests {
@@ -6142,11 +6143,13 @@ func TestNoStructLevelValidation(t *testing.T) {
 	}
 
 	type Outer struct {
-		InnerStruct *Inner `validate:"required,nostructlevel"`
+		InnerStruct    Inner  `validate:"required,nostructlevel"`
+		InnerStructPtr *Inner `validate:"required,nostructlevel"`
 	}
 
 	outer := &Outer{
-		InnerStruct: nil,
+		InnerStructPtr: nil,
+		InnerStruct:    Inner{},
 	}
 
 	validate := New()
@@ -6154,13 +6157,15 @@ func TestNoStructLevelValidation(t *testing.T) {
 	errs := validate.Struct(outer)
 	NotEqual(t, errs, nil)
 	AssertError(t, errs, "Outer.InnerStruct", "Outer.InnerStruct", "InnerStruct", "InnerStruct", "required")
+	AssertError(t, errs, "Outer.InnerStructPtr", "Outer.InnerStructPtr", "InnerStructPtr", "InnerStructPtr", "required")
 
-	inner := &Inner{
+	inner := Inner{
 		Test: "1234",
 	}
 
 	outer = &Outer{
-		InnerStruct: inner,
+		InnerStruct:    inner,
+		InnerStructPtr: &inner,
 	}
 
 	errs = validate.Struct(outer)
@@ -6173,11 +6178,13 @@ func TestStructOnlyValidation(t *testing.T) {
 	}
 
 	type Outer struct {
-		InnerStruct *Inner `validate:"required,structonly"`
+		InnerStruct    Inner  `validate:"required,structonly"`
+		InnerStructPtr *Inner `validate:"required,structonly"`
 	}
 
 	outer := &Outer{
-		InnerStruct: nil,
+		InnerStruct:    Inner{},
+		InnerStructPtr: nil,
 	}
 
 	validate := New()
@@ -6185,13 +6192,15 @@ func TestStructOnlyValidation(t *testing.T) {
 	errs := validate.Struct(outer)
 	NotEqual(t, errs, nil)
 	AssertError(t, errs, "Outer.InnerStruct", "Outer.InnerStruct", "InnerStruct", "InnerStruct", "required")
+	AssertError(t, errs, "Outer.InnerStructPtr", "Outer.InnerStructPtr", "InnerStructPtr", "InnerStructPtr", "required")
 
-	inner := &Inner{
+	inner := Inner{
 		Test: "1234",
 	}
 
 	outer = &Outer{
-		InnerStruct: inner,
+		InnerStruct:    inner,
+		InnerStructPtr: &inner,
 	}
 
 	errs = validate.Struct(outer)
@@ -10823,6 +10832,8 @@ func TestRequiredIf(t *testing.T) {
 		Field6  uint              `validate:"required_if=Field5 1" json:"field_6"`
 		Field7  float32           `validate:"required_if=Field6 1" json:"field_7"`
 		Field8  float64           `validate:"required_if=Field7 1.0" json:"field_8"`
+		Field9  Inner             `validate:"required_if=Field1 test" json:"field_9"`
+		Field10 *Inner            `validate:"required_if=Field1 test" json:"field_10"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field2: &fieldVal,
@@ -10848,6 +10859,8 @@ func TestRequiredIf(t *testing.T) {
 		Field5  string            `validate:"required_if=Field3 1" json:"field_5"`
 		Field6  string            `validate:"required_if=Inner.Field test" json:"field_6"`
 		Field7  string            `validate:"required_if=Inner2.Field test" json:"field_7"`
+		Field8  Inner             `validate:"required_if=Field2 test" json:"field_8"`
+		Field9  *Inner            `validate:"required_if=Field2 test" json:"field_9"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field2: &fieldVal,
@@ -10857,10 +10870,12 @@ func TestRequiredIf(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 3)
+	Equal(t, len(ve), 5)
 	AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_if")
 	AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_if")
 	AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_if")
+	AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_if")
+	AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_if")
 
 	defer func() {
 		if r := recover(); r == nil {
@@ -10897,6 +10912,8 @@ func TestRequiredUnless(t *testing.T) {
 		Field8  float64           `validate:"required_unless=Field7 0.0" json:"field_8"`
 		Field9  bool              `validate:"omitempty" json:"field_9"`
 		Field10 string            `validate:"required_unless=Field9 true" json:"field_10"`
+		Field11 Inner             `validate:"required_unless=Field9 true" json:"field_11"`
+		Field12 *Inner            `validate:"required_unless=Field9 true" json:"field_12"`
 	}{
 		FieldE: "test",
 		Field2: &fieldVal,
@@ -10925,6 +10942,8 @@ func TestRequiredUnless(t *testing.T) {
 		Field7  string            `validate:"required_unless=Inner2.Field test" json:"field_7"`
 		Field8  bool              `validate:"omitempty" json:"field_8"`
 		Field9  string            `validate:"required_unless=Field8 true" json:"field_9"`
+		Field10 Inner             `validate:"required_unless=Field9 true" json:"field_10"`
+		Field11 *Inner            `validate:"required_unless=Field9 true" json:"field_11"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		FieldE: "test",
@@ -10935,11 +10954,13 @@ func TestRequiredUnless(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 4)
+	Equal(t, len(ve), 6)
 	AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_unless")
 	AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_unless")
 	AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_unless")
 	AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_unless")
+	AssertError(t, errs, "Field10", "Field10", "Field10", "Field10", "required_unless")
+	AssertError(t, errs, "Field11", "Field11", "Field11", "Field11", "required_unless")
 
 	defer func() {
 		if r := recover(); r == nil {
@@ -10976,6 +10997,8 @@ func TestSkipUnless(t *testing.T) {
 		Field8  float64           `validate:"skip_unless=Field7 1.0" json:"field_8"`
 		Field9  bool              `validate:"omitempty" json:"field_9"`
 		Field10 string            `validate:"skip_unless=Field9 false" json:"field_10"`
+		Field11 Inner             `validate:"skip_unless=Field9 false" json:"field_11"`
+		Field12 *Inner            `validate:"skip_unless=Field9 false" json:"field_12"`
 	}{
 		FieldE: "test1",
 		Field2: &fieldVal,
@@ -11004,6 +11027,8 @@ func TestSkipUnless(t *testing.T) {
 		Field7  string            `validate:"skip_unless=Inner2.Field test" json:"field_7"`
 		Field8  bool              `validate:"omitempty" json:"field_8"`
 		Field9  string            `validate:"skip_unless=Field8 true" json:"field_9"`
+		Field10 Inner             `validate:"skip_unless=Field8 false" json:"field_10"`
+		Field11 *Inner            `validate:"skip_unless=Field8 false" json:"field_11"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		FieldE: "test1",
@@ -11014,8 +11039,10 @@ func TestSkipUnless(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 1)
+	Equal(t, len(ve), 3)
 	AssertError(t, errs, "Field5", "Field5", "Field5", "Field5", "skip_unless")
+	AssertError(t, errs, "Field10", "Field10", "Field10", "Field10", "skip_unless")
+	AssertError(t, errs, "Field11", "Field11", "Field11", "Field11", "skip_unless")
 
 	test3 := struct {
 		Inner  *Inner
@@ -11055,13 +11082,17 @@ func TestRequiredWith(t *testing.T) {
 		Field2  *string           `validate:"required_with=Field1" json:"field_2"`
 		Field3  map[string]string `validate:"required_with=Field2" json:"field_3"`
 		Field4  interface{}       `validate:"required_with=Field3" json:"field_4"`
-		Field5  string            `validate:"required_with=Inner.Field" json:"field_5"`
+		Field5  string            `validate:"required_with=Field" json:"field_5"`
+		Field6  Inner             `validate:"required_with=Field2" json:"field_6"`
+		Field7  *Inner            `validate:"required_with=Field2" json:"field_7"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field2: &fieldVal,
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: "test",
+		Field6: Inner{Field: &fieldVal},
+		Field7: &Inner{Field: &fieldVal},
 	}
 
 	validate := New()
@@ -11081,6 +11112,8 @@ func TestRequiredWith(t *testing.T) {
 		Field5  string            `validate:"required_with=Field3" json:"field_5"`
 		Field6  string            `validate:"required_with=Inner.Field" json:"field_6"`
 		Field7  string            `validate:"required_with=Inner2.Field" json:"field_7"`
+		Field8  Inner             `validate:"required_with=Field2" json:"field_8"`
+		Field9  *Inner            `validate:"required_with=Field2" json:"field_9"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field2: &fieldVal,
@@ -11090,10 +11123,12 @@ func TestRequiredWith(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 3)
+	Equal(t, len(ve), 5)
 	AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_with")
 	AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_with")
 	AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_with")
+	AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_with")
+	AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_with")
 }
 
 func TestExcludedWith(t *testing.T) {
@@ -11114,6 +11149,8 @@ func TestExcludedWith(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_with=FieldE" json:"field_4"`
 		Field5 string            `validate:"excluded_with=Inner.FieldE" json:"field_5"`
 		Field6 string            `validate:"excluded_with=Inner2.FieldE" json:"field_6"`
+		Field7 Inner             `validate:"excluded_with=FieldE" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_with=FieldE" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field1: fieldVal,
@@ -11140,6 +11177,8 @@ func TestExcludedWith(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_with=Field" json:"field_4"`
 		Field5 string            `validate:"excluded_with=Inner.Field" json:"field_5"`
 		Field6 string            `validate:"excluded_with=Inner2.Field" json:"field_6"`
+		Field7 Inner             `validate:"excluded_with=Field" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_with=Field" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field:  "populated",
@@ -11149,13 +11188,15 @@ func TestExcludedWith(t *testing.T) {
 		Field4: "test",
 		Field5: "test",
 		Field6: "test",
+		Field7: Inner{FieldE: "potato"},
+		Field8: &Inner{FieldE: "potato"},
 	}
 
 	errs = validate.Struct(test2)
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 5)
+	Equal(t, len(ve), 7)
 	for i := 1; i <= 5; i++ {
 		name := fmt.Sprintf("Field%d", i)
 		AssertError(t, errs, name, name, name, name, "excluded_with")
@@ -11172,6 +11213,8 @@ func TestExcludedWith(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_with=FieldE" json:"field_4"`
 		Field5 string            `validate:"excluded_with=Inner.FieldE" json:"field_5"`
 		Field6 string            `validate:"excluded_with=Inner2.FieldE" json:"field_6"`
+		Field7 Inner             `validate:"excluded_with=FieldE" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_with=FieldE" json:"field_8"`
 	}{
 		Inner:  &Inner{FieldE: "populated"},
 		Inner2: &Inner{FieldE: "populated"},
@@ -11201,6 +11244,8 @@ func TestExcludedWithout(t *testing.T) {
 		Field3 map[string]string `validate:"excluded_without=Field" json:"field_3"`
 		Field4 interface{}       `validate:"excluded_without=Field" json:"field_4"`
 		Field5 string            `validate:"excluded_without=Inner.Field" json:"field_5"`
+		Field6 Inner             `validate:"excluded_without=Field" json:"field_6"`
+		Field7 *Inner            `validate:"excluded_without=Field" json:"field_7"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field:  "populated",
@@ -11209,6 +11254,8 @@ func TestExcludedWithout(t *testing.T) {
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: "test",
+		Field6: Inner{FieldE: "potato"},
+		Field7: &Inner{FieldE: "potato"},
 	}
 
 	validate := New()
@@ -11227,6 +11274,8 @@ func TestExcludedWithout(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_without=FieldE" json:"field_4"`
 		Field5 string            `validate:"excluded_without=Inner.FieldE" json:"field_5"`
 		Field6 string            `validate:"excluded_without=Inner2.FieldE" json:"field_6"`
+		Field7 Inner             `validate:"excluded_without=FieldE" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_without=FieldE" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field1: fieldVal,
@@ -11235,13 +11284,15 @@ func TestExcludedWithout(t *testing.T) {
 		Field4: "test",
 		Field5: "test",
 		Field6: "test",
+		Field7: Inner{FieldE: "potato"},
+		Field8: &Inner{FieldE: "potato"},
 	}
 
 	errs = validate.Struct(test2)
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 6)
+	Equal(t, len(ve), 8)
 	for i := 1; i <= 6; i++ {
 		name := fmt.Sprintf("Field%d", i)
 		AssertError(t, errs, name, name, name, name, "excluded_without")
@@ -11257,6 +11308,8 @@ func TestExcludedWithout(t *testing.T) {
 		Field3 map[string]string `validate:"excluded_without=Field" json:"field_3"`
 		Field4 interface{}       `validate:"excluded_without=Field" json:"field_4"`
 		Field5 string            `validate:"excluded_without=Inner.Field" json:"field_5"`
+		Field6 Inner             `validate:"excluded_without=Field" json:"field_6"`
+		Field7 *Inner            `validate:"excluded_without=Field" json:"field_7"`
 	}{
 		Inner: &Inner{Field: &fieldVal},
 		Field: "populated",
@@ -11286,6 +11339,8 @@ func TestExcludedWithAll(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_with_all=FieldE Field" json:"field_4"`
 		Field5 string            `validate:"excluded_with_all=Inner.FieldE" json:"field_5"`
 		Field6 string            `validate:"excluded_with_all=Inner2.FieldE" json:"field_6"`
+		Field7 Inner             `validate:"excluded_with_all=FieldE Field" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_with_all=FieldE Field" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field:  fieldVal,
@@ -11295,6 +11350,8 @@ func TestExcludedWithAll(t *testing.T) {
 		Field4: "test",
 		Field5: "test",
 		Field6: "test",
+		Field7: Inner{FieldE: "potato"},
+		Field8: &Inner{FieldE: "potato"},
 	}
 
 	validate := New()
@@ -11313,6 +11370,8 @@ func TestExcludedWithAll(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_with_all=Field FieldE" json:"field_4"`
 		Field5 string            `validate:"excluded_with_all=Inner.Field" json:"field_5"`
 		Field6 string            `validate:"excluded_with_all=Inner2.Field" json:"field_6"`
+		Field7 Inner             `validate:"excluded_with_all=Field FieldE" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_with_all=Field FieldE" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field:  "populated",
@@ -11323,13 +11382,15 @@ func TestExcludedWithAll(t *testing.T) {
 		Field4: "test",
 		Field5: "test",
 		Field6: "test",
+		Field7: Inner{FieldE: "potato"},
+		Field8: &Inner{FieldE: "potato"},
 	}
 
 	errs = validate.Struct(test2)
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 5)
+	Equal(t, len(ve), 7)
 	for i := 1; i <= 5; i++ {
 		name := fmt.Sprintf("Field%d", i)
 		AssertError(t, errs, name, name, name, name, "excluded_with_all")
@@ -11346,6 +11407,8 @@ func TestExcludedWithAll(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_with_all=FieldE Field" json:"field_4"`
 		Field5 string            `validate:"excluded_with_all=Inner.FieldE" json:"field_5"`
 		Field6 string            `validate:"excluded_with_all=Inner2.FieldE" json:"field_6"`
+		Field7 Inner             `validate:"excluded_with_all=Field FieldE" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_with_all=Field FieldE" json:"field_8"`
 	}{
 		Inner:  &Inner{FieldE: "populated"},
 		Inner2: &Inner{FieldE: "populated"},
@@ -11376,6 +11439,8 @@ func TestExcludedWithoutAll(t *testing.T) {
 		Field3 map[string]string `validate:"excluded_without_all=Field FieldE" json:"field_3"`
 		Field4 interface{}       `validate:"excluded_without_all=Field FieldE" json:"field_4"`
 		Field5 string            `validate:"excluded_without_all=Inner.Field Inner2.Field" json:"field_5"`
+		Field6 Inner             `validate:"excluded_without_all=Field FieldE" json:"field_6"`
+		Field7 *Inner            `validate:"excluded_without_all=Field FieldE" json:"field_7"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Inner2: &Inner{Field: &fieldVal},
@@ -11385,6 +11450,8 @@ func TestExcludedWithoutAll(t *testing.T) {
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: "test",
+		Field6: Inner{FieldE: "potato"},
+		Field7: &Inner{FieldE: "potato"},
 	}
 
 	validate := New()
@@ -11403,6 +11470,8 @@ func TestExcludedWithoutAll(t *testing.T) {
 		Field4 interface{}       `validate:"excluded_without_all=FieldE Field" json:"field_4"`
 		Field5 string            `validate:"excluded_without_all=Inner.FieldE" json:"field_5"`
 		Field6 string            `validate:"excluded_without_all=Inner2.FieldE" json:"field_6"`
+		Field7 Inner             `validate:"excluded_without_all=Field FieldE" json:"field_7"`
+		Field8 *Inner            `validate:"excluded_without_all=Field FieldE" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field1: fieldVal,
@@ -11411,13 +11480,15 @@ func TestExcludedWithoutAll(t *testing.T) {
 		Field4: "test",
 		Field5: "test",
 		Field6: "test",
+		Field7: Inner{FieldE: "potato"},
+		Field8: &Inner{FieldE: "potato"},
 	}
 
 	errs = validate.Struct(test2)
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 6)
+	Equal(t, len(ve), 8)
 	for i := 1; i <= 6; i++ {
 		name := fmt.Sprintf("Field%d", i)
 		AssertError(t, errs, name, name, name, name, "excluded_without_all")
@@ -11433,6 +11504,8 @@ func TestExcludedWithoutAll(t *testing.T) {
 		Field3 map[string]string `validate:"excluded_without_all=Field FieldE" json:"field_3"`
 		Field4 interface{}       `validate:"excluded_without_all=Field FieldE" json:"field_4"`
 		Field5 string            `validate:"excluded_without_all=Inner.Field Inner2.Field" json:"field_5"`
+		Field6 Inner             `validate:"excluded_without_all=Field FieldE" json:"field_6"`
+		Field7 *Inner            `validate:"excluded_without_all=Field FieldE" json:"field_7"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Inner2: &Inner{Field: &fieldVal},
@@ -11461,6 +11534,8 @@ func TestRequiredWithAll(t *testing.T) {
 		Field3  map[string]string `validate:"required_with_all=Field2" json:"field_3"`
 		Field4  interface{}       `validate:"required_with_all=Field3" json:"field_4"`
 		Field5  string            `validate:"required_with_all=Inner.Field" json:"field_5"`
+		Field6  Inner             `validate:"required_with_all=Field1 Field2" json:"field_6"`
+		Field7  *Inner            `validate:"required_with_all=Field1 Field2" json:"field_7"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field1: "test_field1",
@@ -11468,6 +11543,8 @@ func TestRequiredWithAll(t *testing.T) {
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: "test",
+		Field6: Inner{Field: &fieldVal},
+		Field7: &Inner{Field: &fieldVal},
 	}
 
 	validate := New()
@@ -11486,6 +11563,8 @@ func TestRequiredWithAll(t *testing.T) {
 		Field4  interface{}       `validate:"required_with_all=Field1 FieldE" json:"field_4"`
 		Field5  string            `validate:"required_with_all=Inner.Field Field2" json:"field_5"`
 		Field6  string            `validate:"required_with_all=Inner2.Field Field2" json:"field_6"`
+		Field7  Inner             `validate:"required_with_all=Inner.Field Field2" json:"field_7"`
+		Field8  *Inner            `validate:"required_with_all=Inner.Field Field2" json:"field_8"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field2: &fieldVal,
@@ -11495,9 +11574,11 @@ func TestRequiredWithAll(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 2)
+	Equal(t, len(ve), 4)
 	AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_with_all")
 	AssertError(t, errs, "Field5", "Field5", "Field5", "Field5", "required_with_all")
+	AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_with_all")
+	AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_with_all")
 }
 
 func TestRequiredWithout(t *testing.T) {
@@ -11513,12 +11594,16 @@ func TestRequiredWithout(t *testing.T) {
 		Field3 map[string]string `validate:"required_without=Field2" json:"field_3"`
 		Field4 interface{}       `validate:"required_without=Field3" json:"field_4"`
 		Field5 string            `validate:"required_without=Field3" json:"field_5"`
+		Field6 Inner             `validate:"required_without=Field1" json:"field_6"`
+		Field7 *Inner            `validate:"required_without=Field1" json:"field_7"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		Field2: &fieldVal,
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: "test",
+		Field6: Inner{Field: &fieldVal},
+		Field7: &Inner{Field: &fieldVal},
 	}
 
 	validate := New()
@@ -11527,16 +11612,18 @@ func TestRequiredWithout(t *testing.T) {
 	Equal(t, errs, nil)
 
 	test2 := struct {
-		Inner  *Inner
-		Inner2 *Inner
-		Field1 string            `json:"field_1"`
-		Field2 *string           `validate:"required_without=Field1" json:"field_2"`
-		Field3 map[string]string `validate:"required_without=Field2" json:"field_3"`
-		Field4 interface{}       `validate:"required_without=Field3" json:"field_4"`
-		Field5 string            `validate:"required_without=Field3" json:"field_5"`
-		Field6 string            `validate:"required_without=Field1" json:"field_6"`
-		Field7 string            `validate:"required_without=Inner.Field" json:"field_7"`
-		Field8 string            `validate:"required_without=Inner.Field" json:"field_8"`
+		Inner   *Inner
+		Inner2  *Inner
+		Field1  string            `json:"field_1"`
+		Field2  *string           `validate:"required_without=Field1" json:"field_2"`
+		Field3  map[string]string `validate:"required_without=Field2" json:"field_3"`
+		Field4  interface{}       `validate:"required_without=Field3" json:"field_4"`
+		Field5  string            `validate:"required_without=Field3" json:"field_5"`
+		Field6  string            `validate:"required_without=Field1" json:"field_6"`
+		Field7  string            `validate:"required_without=Inner.Field" json:"field_7"`
+		Field8  string            `validate:"required_without=Inner.Field" json:"field_8"`
+		Field9  Inner             `validate:"required_without=Field1" json:"field_9"`
+		Field10 *Inner            `validate:"required_without=Field1" json:"field_10"`
 	}{
 		Inner:  &Inner{},
 		Field3: map[string]string{"key": "val"},
@@ -11548,11 +11635,13 @@ func TestRequiredWithout(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 4)
+	Equal(t, len(ve), 6)
 	AssertError(t, errs, "Field2", "Field2", "Field2", "Field2", "required_without")
 	AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_without")
 	AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_without")
 	AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_without")
+	AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_without")
+	AssertError(t, errs, "Field10", "Field10", "Field10", "Field10", "required_without")
 
 	test3 := struct {
 		Field1 *string `validate:"required_without=Field2,omitempty,min=1" json:"field_1"`
@@ -11566,6 +11655,10 @@ func TestRequiredWithout(t *testing.T) {
 }
 
 func TestRequiredWithoutAll(t *testing.T) {
+	type nested struct {
+		value string
+	}
+
 	fieldVal := "test"
 	test := struct {
 		Field1 string            `validate:"omitempty" json:"field_1"`
@@ -11573,12 +11666,16 @@ func TestRequiredWithoutAll(t *testing.T) {
 		Field3 map[string]string `validate:"required_without_all=Field2" json:"field_3"`
 		Field4 interface{}       `validate:"required_without_all=Field3" json:"field_4"`
 		Field5 string            `validate:"required_without_all=Field3" json:"field_5"`
+		Field6 nested            `validate:"required_without_all=Field1" json:"field_6"`
+		Field7 *nested           `validate:"required_without_all=Field1" json:"field_7"`
 	}{
 		Field1: "",
 		Field2: &fieldVal,
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: "test",
+		Field6: nested{"potato"},
+		Field7: &nested{"potato"},
 	}
 
 	validate := New()
@@ -11593,6 +11690,8 @@ func TestRequiredWithoutAll(t *testing.T) {
 		Field4 interface{}       `validate:"required_without_all=Field3" json:"field_4"`
 		Field5 string            `validate:"required_without_all=Field3" json:"field_5"`
 		Field6 string            `validate:"required_without_all=Field1 Field3" json:"field_6"`
+		Field7 nested            `validate:"required_without_all=Field1" json:"field_7"`
+		Field8 *nested           `validate:"required_without_all=Field1" json:"field_8"`
 	}{
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
@@ -11603,8 +11702,10 @@ func TestRequiredWithoutAll(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 1)
+	Equal(t, len(ve), 3)
 	AssertError(t, errs, "Field2", "Field2", "Field2", "Field2", "required_without_all")
+	AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_without_all")
+	AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_without_all")
 }
 
 func TestExcludedIf(t *testing.T) {
@@ -13135,3 +13236,180 @@ func TestCronExpressionValidation(t *testing.T) {
 		}
 	}
 }
+
+func TestNestedStructValidation(t *testing.T) {
+	validator := New()
+
+	t.Run("required", func(t *testing.T) {
+		type (
+			value struct {
+				Field string
+			}
+			topLevel struct {
+				Nested value `validate:"required"`
+			}
+		)
+
+		var validationErrs ValidationErrors
+		if errs := validator.Struct(topLevel{}); errs != nil {
+			validationErrs = errs.(ValidationErrors)
+		}
+
+		Equal(t, 1, len(validationErrs))
+		AssertError(t, validationErrs, "topLevel.Nested", "topLevel.Nested", "Nested", "Nested", "required")
+
+		Equal(t, validator.Struct(topLevel{value{"potato"}}), nil)
+	})
+
+	t.Run("omitempty", func(t *testing.T) {
+		type (
+			value struct {
+				Field string
+			}
+			topLevel struct {
+				Nested value `validate:"omitempty,required"`
+			}
+		)
+
+		errs := validator.Struct(topLevel{})
+		Equal(t, errs, nil)
+	})
+
+	t.Run("excluded_if", func(t *testing.T) {
+		type (
+			value struct {
+				Field string
+			}
+			topLevel struct {
+				Field  string
+				Nested value `validate:"excluded_if=Field potato"`
+			}
+		)
+
+		errs := validator.Struct(topLevel{Field: "test", Nested: value{"potato"}})
+		Equal(t, errs, nil)
+
+		errs = validator.Struct(topLevel{Field: "potato"})
+		Equal(t, errs, nil)
+
+		errs = validator.Struct(topLevel{Field: "potato", Nested: value{"potato"}})
+		AssertError(t, errs, "topLevel.Nested", "topLevel.Nested", "Nested", "Nested", "excluded_if")
+	})
+
+	t.Run("excluded_unless", func(t *testing.T) {
+		type (
+			value struct {
+				Field string
+			}
+			topLevel struct {
+				Field  string
+				Nested value `validate:"excluded_unless=Field potato"`
+			}
+		)
+
+		errs := validator.Struct(topLevel{Field: "test"})
+		Equal(t, errs, nil)
+
+		errs = validator.Struct(topLevel{Field: "potato", Nested: value{"potato"}})
+		Equal(t, errs, nil)
+
+		errs = validator.Struct(topLevel{Field: "test", Nested: value{"potato"}})
+		AssertError(t, errs, "topLevel.Nested", "topLevel.Nested", "Nested", "Nested", "excluded_unless")
+	})
+
+	t.Run("nonComparableField", func(t *testing.T) {
+		type (
+			value struct {
+				Field []string
+			}
+			topLevel struct {
+				Nested value `validate:"required"`
+			}
+		)
+
+		errs := validator.Struct(topLevel{value{[]string{}}})
+		Equal(t, errs, nil)
+	})
+
+	type (
+		veggyBasket struct {
+			Root   string
+			Squash string `validate:"required"`
+		}
+		testErr struct {
+			path string
+			tag  string
+		}
+		test struct {
+			name  string
+			err   testErr
+			value veggyBasket
+		}
+	)
+
+	if err := validator.RegisterValidation("veggy", func(f FieldLevel) bool {
+		v, ok := f.Field().Interface().(veggyBasket)
+		if !ok || v.Root != "potato" {
+			return false
+		}
+		return true
+	}); err != nil {
+		t.Fatal(fmt.Errorf("failed to register potato tag: %w", err))
+	}
+
+	tests := []test{
+		{
+			name:  "valid",
+			value: veggyBasket{"potato", "zucchini"},
+		}, {
+			name:  "failedCustomTag",
+			value: veggyBasket{"zucchini", "potato"},
+			err:   testErr{"topLevel.VeggyBasket", "veggy"},
+		}, {
+			name:  "failedInnerField",
+			value: veggyBasket{"potato", ""},
+			err:   testErr{"topLevel.VeggyBasket.Squash", "required"},
+		}, {
+			name:  "customTagFailurePriorityCheck",
+			value: veggyBasket{"zucchini", ""},
+			err:   testErr{"topLevel.VeggyBasket", "veggy"},
+		},
+	}
+
+	var evaluateTest = func(tt test, errs error) {
+		if tt.err != (testErr{}) && errs != nil {
+			Equal(t, len(errs.(ValidationErrors)), 1)
+
+			segments := strings.Split(tt.err.path, ".")
+			fieldName := segments[len(segments)-1]
+			AssertError(t, errs, tt.err.path, tt.err.path, fieldName, fieldName, tt.err.tag)
+		}
+
+		shouldFail := tt.err != (testErr{})
+		hasFailed := errs != nil
+		if shouldFail != hasFailed {
+			t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, errs)
+		}
+	}
+
+	for _, tt := range tests {
+		type topLevel struct {
+			VeggyBasket veggyBasket `validate:"veggy"`
+		}
+
+		t.Run(tt.name, func(t *testing.T) {
+			evaluateTest(tt, validator.Struct(topLevel{tt.value}))
+		})
+	}
+
+	// Also test on struct pointers
+	for _, tt := range tests {
+		type topLevel struct {
+			VeggyBasket *veggyBasket `validate:"veggy"`
+		}
+
+		t.Run(tt.name+"Ptr", func(t *testing.T) {
+			evaluateTest(tt, validator.Struct(topLevel{&tt.value}))
+		})
+	}
+}