Repository: go-playground/validator (go)

Issue:
Custom validation tag is ignored when validating a field that is of type struct
### Package version eg. v8, v9: 

v9

### Issue, Question or Enhancement:

The custom validation tag appears to have no impact when it is used against a field that is of type struct. If the tag is not registered, the validator library panics as expected, however if the tag is registered and deliberately set to return false, I would expect validation to fail.

However it does not.

Is this intended behaviour? Can field validation not be performed in this way? If I was to use my custom tag against a standard system type, such as a string, the validation fails as expected.

Apologies if this is covered in the documentation somewhere.

### Code sample, to showcase or reproduce:

```go
package main

import (
	"fmt"
	"time"

	validator "gopkg.in/go-playground/validator.v9"
)

// We want to validate the ValidUntil is equal to or after ValidFrom

type Record struct {
	ValidFrom  ExtendedDate `validate:"required"`
	ValidUntil ExtendedDate `validate:"required,gteEpoch=ValidFrom"`
}

type ExtendedDate struct {
	Epoch   int64     `validate:"required"`
	Iso6801 time.Time `validate:"required"`
}

var validate *validator.Validate

func main() {

	record := Record{
		ValidFrom: ExtendedDate{
			Epoch:   1,
			Iso6801: time.Now(),
		},
		ValidUntil: ExtendedDate{
			Epoch:   1,
			Iso6801: time.Now().AddDate(0, 0, -1),
		},
	}

	validate = validator.New()
	validate.RegisterValidation("gteEpoch", gteEpoch)

	err := validate.Struct(record)

	// This should fail, but it does not?
	if err != nil {
		fmt.Printf("%v\n", err.Error())
	}
}

func gteEpoch(fl validator.FieldLevel) bool {
	// Perform required validation checks here...

	// Deliberately fail here for this example
	return false
}

```


Pull request description:
Feature: nested struct validation
## Fixes #367, #906

**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.

A test has been added for custom tags, however I was not brave enough to actually update the tests for all required/excluded tag variants before getting an initial feedback, but I'm willing to do so if this ever gets any further.

Same goes for documentation.

The implementation supports both struct and struct pointer validations for custom tags and all required/excluded tag variants.

Struct validity is evaluated first and fields are evaluated only if the struct is valid, though I'm not sure if this is the desired behavior.

@go-playground/validator-maintainers

Patch:
diff --git a/baked_in.go b/baked_in.go
index e676f1d..ed0cf3b 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -1710,7 +1710,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 +1734,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..8ca2c2a 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -6142,11 +6142,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 +6156,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 +6177,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 +6191,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 +10831,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 +10858,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 +10869,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 +10911,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 +10941,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 +10953,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 +10996,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 +11026,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 +11038,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 +11081,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 +11111,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 +11122,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 +11148,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 +11176,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 +11187,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 +11212,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 +11243,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 +11253,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 +11273,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 +11283,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 +11307,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 +11338,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 +11349,8 @@ func TestExcludedWithAll(t *testing.T) {
 		Field4: "test",
 		Field5: "test",
 		Field6: "test",
+		Field7: Inner{FieldE: "potato"},
+		Field8: &Inner{FieldE: "potato"},
 	}
 
 	validate := New()
@@ -11313,6 +11369,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 +11381,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 +11406,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 +11438,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 +11449,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 +11469,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 +11479,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 +11503,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 +11533,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 +11542,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 +11562,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 +11573,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 +11593,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 +11611,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 +11634,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 +11654,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 +11665,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 +11689,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 +11701,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 +13235,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}))
+		})
+	}
+}