Repository: go-playground/validator
Pull request #1125 (2023-07-14 21:43:58)

Feat: add SpiceDB validations
## Fixes Or Enhances
Adds support for validating [SpiceDB](https://github.com/authzed/spicedb) object ids, types, and permissions

**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/61360734/badge)](https://coveralls.io/builds/61360734)

coverage: 73.985% (+0.01%) from 73.972% when pulling **8e91f6fdcd20e23104992dcf5c20935827a686db on wscalf:add-spicedb-tags** into **bd1113d5c1901c5205fc0a7af031a11268ff13ee on go-playground:master**.


Patch:
diff --git a/README.md b/README.md
index 520661d..9b93e61 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)
@@ -158,6 +158,7 @@ Baked-in Validations
 | credit_card | Credit Card Number |
 | mongodb | MongoDB ObjectID |
 | cron | Cron |
+| spicedb | SpiceDb ObjectID/Permission/Type |
 | datetime | Datetime |
 | e164 | e164 formatted phone number |
 | email | E-mail String
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..f7b55ab 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -230,6 +230,7 @@ var (
 		"luhn_checksum":                 hasLuhnChecksum,
 		"mongodb":                       isMongoDB,
 		"cron":                          isCron,
+		"spicedb":                       isSpiceDB,
 	}
 )
 
@@ -1561,6 +1562,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 +1715,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 +1739,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()
 	}
 }
 
@@ -2808,6 +2813,23 @@ func isMongoDB(fl FieldLevel) bool {
 	return mongodbRegex.MatchString(val)
 }
 
+// isSpiceDB is the validation function for validating if the current field's value is valid for use with Authzed SpiceDB in the indicated way
+func isSpiceDB(fl FieldLevel) bool {
+	val := fl.Field().String()
+	param := fl.Param()
+
+	switch param {
+	case "permission":
+		return spicedbPermissionRegex.MatchString(val)
+	case "type":
+		return spicedbTypeRegex.MatchString(val)
+	case "id", "":
+		return spicedbIDRegex.MatchString(val)
+	}
+
+	panic("Unrecognized parameter: " + param)
+}
+
 // isCreditCard is the validation function for validating if the current field's value is a valid credit card number
 func isCreditCard(fl FieldLevel) bool {
 	val := fl.Field().String()
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..d1eff50 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
 
@@ -879,8 +879,6 @@ This is done using os.Stat and github.com/gabriel-vasile/mimetype
 
 	Usage: image
 
-# URL String
-
 # File Path
 
 This validates that a string value contains a valid file path but does not
@@ -1384,6 +1382,12 @@ This validates that a string value contains a valid cron expression.
 
 	Usage: cron
 
+# SpiceDb ObjectID/Permission/Object Type
+
+This validates that a string is valid for use with SpiceDb for the indicated purpose. If no purpose is given, a purpose of 'id' is assumed.
+
+	Usage: spicedb=id|permission|type
+
 # Alias Validators and Tags
 
 Alias Validators and Tags
diff --git a/regexes.go b/regexes.go
index ba450b3..6c8f985 100644
--- a/regexes.go
+++ b/regexes.go
@@ -68,6 +68,9 @@ const (
 	cveRegexString                   = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html
 	mongodbRegexString               = "^[a-f\\d]{24}$"
 	cronRegexString                  = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})`
+	spicedbIDRegexString             = `^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$`
+	spicedbPermissionRegexString     = "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$"
+	spicedbTypeRegexString           = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$"
 )
 
 var (
@@ -134,4 +137,7 @@ var (
 	cveRegex                   = regexp.MustCompile(cveRegexString)
 	mongodbRegex               = regexp.MustCompile(mongodbRegexString)
 	cronRegex                  = regexp.MustCompile(cronRegexString)
+	spicedbIDRegex             = regexp.MustCompile(spicedbIDRegexString)
+	spicedbPermissionRegex     = regexp.MustCompile(spicedbPermissionRegexString)
+	spicedbTypeRegex           = regexp.MustCompile(spicedbTypeRegexString)
 )
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..0887827 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) {
@@ -12993,6 +13094,80 @@ func TestMongoDBObjectIDFormatValidation(t *testing.T) {
 	}
 }
 
+func TestSpiceDBValueFormatValidation(t *testing.T) {
+	tests := []struct {
+		value    string
+		tag      string
+		expected bool
+	}{
+		//Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
+		{"*", "spicedb=id", true},
+		{`azAZ09_|/-=+`, "spicedb=id", true},
+		{`a*`, "spicedb=id", false},
+		{`/`, "spicedb=id", true},
+		{"*", "spicedb", true},
+
+		//Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
+		{"a", "spicedb=permission", false},
+		{"1", "spicedb=permission", false},
+		{"a1", "spicedb=permission", false},
+		{"a_b", "spicedb=permission", true},
+		{"A_b", "spicedb=permission", false},
+		{"a_B", "spicedb=permission", false},
+		{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", true},
+		{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", false},
+
+		//Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
+		{"a", "spicedb=type", false},
+		{"1", "spicedb=type", false},
+		{"a1", "spicedb=type", false},
+		{"a_b", "spicedb=type", true},
+		{"A_b", "spicedb=type", false},
+		{"a_B", "spicedb=type", false},
+		{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=type", true},
+		{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=type", false},
+
+		{`a_b/a`, "spicedb=type", false},
+		{`a_b/1`, "spicedb=type", false},
+		{`a_b/a1`, "spicedb=type", false},
+		{`a_b/a_b`, "spicedb=type", true},
+		{`a_b/A_b`, "spicedb=type", false},
+		{`a_b/a_B`, "spicedb=type", false},
+		{`a_b/abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz`, "spicedb=type", true},
+		{`a_b/abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz`, "spicedb=type", false},
+
+		{`a/a_b`, "spicedb=type", false},
+		{`1/a_b`, "spicedb=type", false},
+		{`a1/a_b`, "spicedb=type", false},
+		{`a_b/a_b`, "spicedb=type", true},
+		{`A_b/a_b`, "spicedb=type", false},
+		{`a_B/a_b`, "spicedb=type", false},
+		{`abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxy/a_b`, "spicedb=type", true},
+		{`abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz/a_b`, "spicedb=type", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+		errs := validate.Var(test.value, test.tag)
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d spicedb failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d spicedb - expected error but there was none.", i)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "spicedb" {
+					t.Fatalf("Index: %d spicedb failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+}
+
 func TestCreditCardFormatValidation(t *testing.T) {
 	tests := []struct {
 		value    string `validate:"credit_card"`
@@ -13135,3 +13310,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}))
+		})
+	}
+}