Repository: go-playground/validator
Pull request #1417 (2025-04-16 19:01:22)

Update workflow.yml to support 2 most recent major versions
## Fixes Or Enhances

Based on the https://github.com/go-playground/validator?tab=readme-ov-file#maintenance-and-support-for-sdk-major-versions

**Make sure that you've checked the boxes below before you submit PR:**
- [ ] Tests exist or have been written that cover this particular change.

@go-playground/validator-maintainers

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

coverage: 73.607%. remained the same
when pulling **5fc7302efbca60a97117f27f82834426ebbd13ae on nodivbyzero:github-action-support-2-major-go-versions**
into **0540a5e4c0b2012fbb4718cb80fd5b5d08911c7e on go-playground:master**.


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..60f396d
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,24 @@
+on:
+  push:
+    branches:
+      - master
+
+name: Test
+jobs:
+  test:
+    strategy:
+      matrix:
+        go-version: [1.21.x, 1.22.x, 1.23.x, 1.24.x]
+        os: [ubuntu-latest]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Install Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ matrix.go-version }}
+
+      - name: Test
+        run: go test -race ./...
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 076c8b9..5b4c082 100644
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -8,7 +8,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.22.x,1.23.x, 1.24.x]
+        go-version: [1.23.x, 1.24.x]
         os: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.os }}
     steps:
@@ -24,7 +24,7 @@ jobs:
         run: go test -race -covermode=atomic -coverprofile="profile.cov" ./...
 
       - name: Send Coverage
-        if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.23.x'
+        if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24.x'
         uses: shogo82148/actions-goveralls@v1
         with:
           path-to-profile: profile.cov
@@ -38,6 +38,6 @@ jobs:
         with:
           go-version: 1.24.x
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v7
+        uses: golangci/golangci-lint-action@v8
         with:
-          version: latest
+          version: latest
\ No newline at end of file
diff --git a/.golangci.yaml b/.golangci.yaml
index d3d8e39..dd9c05c 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -2,6 +2,8 @@ version: "2"
 linters:
   default: all
   disable:
+    - noinlineerr
+    - wsl_v5
     - copyloopvar
     - cyclop
     - depguard
diff --git a/_examples/http-transalations/main.go b/_examples/http-transalations/main.go
new file mode 100644
index 0000000..0d93b9c
--- /dev/null
+++ b/_examples/http-transalations/main.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"strings"
+
+	"github.com/go-playground/locales/en"
+	"github.com/go-playground/locales/zh"
+	"github.com/go-playground/locales/zh_Hant_TW"
+	ut "github.com/go-playground/universal-translator"
+	"github.com/go-playground/validator/v10"
+	en_translations "github.com/go-playground/validator/v10/translations/en"
+	zh_translations "github.com/go-playground/validator/v10/translations/zh"
+	zh_tw_translations "github.com/go-playground/validator/v10/translations/zh_tw"
+)
+
+var uni *ut.UniversalTranslator
+
+// This example showcases how to use the Validator and UniversalTranslator with both Simplified and Traditional Chinese languages.
+// To run the example:
+// Step 1: go run _examples/http-transalations/main.go
+// Step 2 - Simplified Chinese: curl -d '{"first_name":"foo"}' -H "Accept-Language: zh" -H "Content-Type: application/json" -X POST http://localhost:8081/users
+// Step 3 - Traditional Chinese: curl -d '{"first_name":"foo"}' -H "Accept-Language: zh-Hant-TW" -H "Content-Type: application/json" -X POST http://localhost:8081/users
+func main() {
+	validate := validator.New()
+	en := en.New()
+	uni = ut.New(en, en, zh.New(), zh_Hant_TW.New())
+
+	validate = validator.New()
+	enTrans, _ := uni.GetTranslator("en")
+	en_translations.RegisterDefaultTranslations(validate, enTrans)
+	zhTrans, _ := uni.GetTranslator("zh")
+	zh_translations.RegisterDefaultTranslations(validate, zhTrans)
+	zhHantTrans, _ := uni.GetTranslator("zh_Hant_TW")
+	zh_tw_translations.RegisterDefaultTranslations(validate, zhHantTrans)
+
+	type User struct {
+		FirstName string `json:"first_name" validate:"required"`
+		LastName  string `json:"last_name" validate:"required"`
+	}
+
+	http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+		// ... fill user value
+		var user User
+
+		// Header Accept-Language value is en or zh
+		trans, _ := uni.GetTranslator(strings.Replace(r.Header.Get("Accept-Language"), "-", "_", -1))
+		if err := validate.Struct(&user); err != nil {
+			var errs validator.ValidationErrors
+			var httpErrors []validator.ValidationErrorsTranslations
+			if errors.As(err, &errs) {
+				httpErrors = append(httpErrors, errs.Translate(trans))
+			}
+			r, _ := json.Marshal(httpErrors)
+			w.Write(r)
+		}
+	})
+
+	http.ListenAndServe(":8081", nil)
+}
diff --git a/_examples/struct-level/main.go b/_examples/struct-level/main.go
index faba03f..a0cdf54 100644
--- a/_examples/struct-level/main.go
+++ b/_examples/struct-level/main.go
@@ -156,7 +156,7 @@ func main() {
 }
 
 // UserStructLevelValidation contains custom struct level validations that don't always
-// make sense at the field validation level. For Example this function validates that either
+// make sense at the field validation level. For example, this function validates that either
 // FirstName or LastName exist; could have done that with a custom field validation but then
 // would have had to add it to both fields duplicating the logic + overhead, this way it's
 // only validated once.
diff --git a/_examples/validate_fn/go.mod b/_examples/validate_fn/go.mod
index b9077c8..8d6da51 100644
--- a/_examples/validate_fn/go.mod
+++ b/_examples/validate_fn/go.mod
@@ -1,6 +1,8 @@
 module github.com/peczenyj/validator/_examples/validate_fn
 
-go 1.20
+go 1.23.0
+
+toolchain go1.24.4
 
 replace github.com/go-playground/validator/v10 => ../../../validator
 
@@ -11,8 +13,8 @@ require (
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
-	golang.org/x/crypto v0.33.0 // indirect
-	golang.org/x/net v0.34.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/text v0.23.0 // indirect
 )
diff --git a/_examples/validate_fn/go.sum b/_examples/validate_fn/go.sum
index 3533f3e..acb0dc1 100644
--- a/_examples/validate_fn/go.sum
+++ b/_examples/validate_fn/go.sum
@@ -12,10 +12,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
 golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
 golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/baked_in.go b/baked_in.go
index 2f4ae0c..c968ad4 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -297,7 +297,7 @@ func isOneOf(fl FieldLevel) bool {
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		v = strconv.FormatUint(field.Uint(), 10)
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 	for i := 0; i < len(vals); i++ {
 		if vals[i] == v {
@@ -313,7 +313,7 @@ func isOneOfCI(fl FieldLevel) bool {
 	field := fl.Field()
 
 	if field.Kind() != reflect.String {
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 	v := field.String()
 	for _, val := range vals {
@@ -387,13 +387,13 @@ func isUnique(fl FieldLevel) bool {
 			}
 
 			if uniqueField.Kind() != field.Kind() {
-				panic(fmt.Sprintf("Bad field type %T:%T", field.Interface(), uniqueField.Interface()))
+				panic(fmt.Sprintf("Bad field type %s:%s", field.Type(), uniqueField.Type()))
 			}
 
-			return field.Interface() != uniqueField.Interface()
+			return getValue(field) != getValue(uniqueField)
 		}
 
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 }
 
@@ -474,7 +474,7 @@ func isLongitude(fl FieldLevel) bool {
 	case reflect.Float64:
 		v = strconv.FormatFloat(field.Float(), 'f', -1, 64)
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 
 	return longitudeRegex().MatchString(v)
@@ -497,7 +497,7 @@ func isLatitude(fl FieldLevel) bool {
 	case reflect.Float64:
 		v = strconv.FormatFloat(field.Float(), 'f', -1, 64)
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 
 	return latitudeRegex().MatchString(v)
@@ -968,8 +968,8 @@ func isNeField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && currentField.Type().ConvertibleTo(timeType) {
-			t := currentField.Interface().(time.Time)
-			fieldTime := field.Interface().(time.Time)
+			t := getValue(currentField).(time.Time)
+			fieldTime := getValue(field).(time.Time)
 
 			return !fieldTime.Equal(t)
 		}
@@ -1023,8 +1023,8 @@ func isLteCrossStructField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && topField.Type().ConvertibleTo(timeType) {
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
-			topTime := topField.Convert(timeType).Interface().(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
+			topTime := getValue(topField.Convert(timeType)).(time.Time)
 
 			return fieldTime.Before(topTime) || fieldTime.Equal(topTime)
 		}
@@ -1068,8 +1068,8 @@ func isLtCrossStructField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && topField.Type().ConvertibleTo(timeType) {
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
-			topTime := topField.Convert(timeType).Interface().(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
+			topTime := getValue(topField.Convert(timeType)).(time.Time)
 
 			return fieldTime.Before(topTime)
 		}
@@ -1112,8 +1112,8 @@ func isGteCrossStructField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && topField.Type().ConvertibleTo(timeType) {
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
-			topTime := topField.Convert(timeType).Interface().(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
+			topTime := getValue(topField.Convert(timeType)).(time.Time)
 
 			return fieldTime.After(topTime) || fieldTime.Equal(topTime)
 		}
@@ -1156,8 +1156,8 @@ func isGtCrossStructField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && topField.Type().ConvertibleTo(timeType) {
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
-			topTime := topField.Convert(timeType).Interface().(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
+			topTime := getValue(topField.Convert(timeType)).(time.Time)
 
 			return fieldTime.After(topTime)
 		}
@@ -1203,8 +1203,8 @@ func isNeCrossStructField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && topField.Type().ConvertibleTo(timeType) {
-			t := field.Convert(timeType).Interface().(time.Time)
-			fieldTime := topField.Convert(timeType).Interface().(time.Time)
+			t := getValue(field.Convert(timeType)).(time.Time)
+			fieldTime := getValue(topField.Convert(timeType)).(time.Time)
 
 			return !fieldTime.Equal(t)
 		}
@@ -1250,8 +1250,8 @@ func isEqCrossStructField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && topField.Type().ConvertibleTo(timeType) {
-			t := field.Convert(timeType).Interface().(time.Time)
-			fieldTime := topField.Convert(timeType).Interface().(time.Time)
+			t := getValue(field.Convert(timeType)).(time.Time)
+			fieldTime := getValue(topField.Convert(timeType)).(time.Time)
 
 			return fieldTime.Equal(t)
 		}
@@ -1297,8 +1297,8 @@ func isEqField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && currentField.Type().ConvertibleTo(timeType) {
-			t := currentField.Convert(timeType).Interface().(time.Time)
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(currentField.Convert(timeType)).(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
 
 			return fieldTime.Equal(t)
 		}
@@ -1353,7 +1353,7 @@ func isEq(fl FieldLevel) bool {
 		return field.Bool() == p
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isEqIgnoreCase is the validation function for validating if the current field's string value is
@@ -1368,7 +1368,7 @@ func isEqIgnoreCase(fl FieldLevel) bool {
 		return strings.EqualFold(field.String(), param)
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isPostcodeByIso3166Alpha2 validates by value which is country code in iso 3166 alpha 2
@@ -1402,7 +1402,7 @@ func isPostcodeByIso3166Alpha2Field(fl FieldLevel) bool {
 	}
 
 	if kind != reflect.String {
-		panic(fmt.Sprintf("Bad field type %T", currentField.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", currentField.Type()))
 	}
 
 	postcodeRegexInit.Do(initPostcodes)
@@ -1458,16 +1458,7 @@ func isURI(fl FieldLevel) bool {
 		return err == nil
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
-}
-
-// isFileURL is the helper function for validating if the `path` valid file URL as per RFC8089
-func isFileURL(path string) bool {
-	if !strings.HasPrefix(path, "file:/") {
-		return false
-	}
-	_, err := url.ParseRequestURI(path)
-	return err == nil
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isURL is the validation function for validating if the current field's value is a valid URL.
@@ -1483,23 +1474,20 @@ func isURL(fl FieldLevel) bool {
 			return false
 		}
 
-		if isFileURL(s) {
-			return true
-		}
-
 		url, err := url.Parse(s)
 		if err != nil || url.Scheme == "" {
 			return false
 		}
+		isFileScheme := url.Scheme == "file"
 
-		if url.Host == "" && url.Fragment == "" && url.Opaque == "" {
+		if (isFileScheme && (len(url.Path) == 0 || url.Path == "/")) || (!isFileScheme && len(url.Host) == 0 && len(url.Fragment) == 0 && len(url.Opaque) == 0) {
 			return false
 		}
 
 		return true
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isHttpURL is the validation function for validating if the current field's value is a valid HTTP(s) URL.
@@ -1522,7 +1510,7 @@ func isHttpURL(fl FieldLevel) bool {
 		return url.Scheme == "http" || url.Scheme == "https"
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isUrnRFC2141 is the validation function for validating if the current field's value is a valid URN as per RFC 2141.
@@ -1539,7 +1527,7 @@ func isUrnRFC2141(fl FieldLevel) bool {
 		return match
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isFile is the validation function for validating if the current field's value is a valid existing file path.
@@ -1556,7 +1544,7 @@ func isFile(fl FieldLevel) bool {
 		return !fileInfo.IsDir()
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isImage is the validation function for validating if the current field's value contains the path to a valid image file
@@ -1618,7 +1606,8 @@ func isImage(fl FieldLevel) bool {
 			return true
 		}
 	}
-	return false
+
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isFilePath is the validation function for validating if the current field's value is a valid file path.
@@ -1672,7 +1661,7 @@ func isFilePath(fl FieldLevel) bool {
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number.
@@ -1782,7 +1771,7 @@ func hasValue(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
 		return !field.IsNil()
 	default:
-		if fl.(*validate).fldIsPointer && field.Interface() != nil {
+		if fl.(*validate).fldIsPointer && getValue(field) != nil {
 			return true
 		}
 		return field.IsValid() && !field.IsZero()
@@ -1793,10 +1782,13 @@ func hasValue(fl FieldLevel) bool {
 func hasNotZeroValue(fl FieldLevel) bool {
 	field := fl.Field()
 	switch field.Kind() {
-	case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
+	case reflect.Slice, reflect.Map:
+		// For slices and maps, consider them "not zero" only if they're both non-nil AND have elements
+		return !field.IsNil() && field.Len() > 0
+	case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
 		return !field.IsNil()
 	default:
-		if fl.(*validate).fldIsPointer && field.Interface() != nil {
+		if fl.(*validate).fldIsPointer && getValue(field) != nil {
 			return !field.IsZero()
 		}
 		return field.IsValid() && !field.IsZero()
@@ -1820,7 +1812,7 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
 	case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
 		return field.IsNil()
 	default:
-		if nullable && field.Interface() != nil {
+		if nullable && getValue(field) != nil {
 			return false
 		}
 		return field.IsValid() && field.IsZero()
@@ -1849,7 +1841,13 @@ func requireCheckFieldValue(
 	case reflect.Float64:
 		return field.Float() == asFloat64(value)
 
-	case reflect.Slice, reflect.Map, reflect.Array:
+	case reflect.Slice, reflect.Map:
+		if value == "nil" {
+			return field.IsNil()
+		}
+		return int64(field.Len()) == asInt(value)
+	case reflect.Array:
+		// Arrays can't be nil, so only compare lengths
 		return int64(field.Len()) == asInt(value)
 
 	case reflect.Bool:
@@ -2004,8 +2002,11 @@ func excludedWithout(fl FieldLevel) bool {
 // requiredWithout is the validation function
 // The field under validation must be present and not empty only when any of the other specified fields are not present.
 func requiredWithout(fl FieldLevel) bool {
-	if requireCheckFieldKind(fl, strings.TrimSpace(fl.Param()), true) {
-		return hasValue(fl)
+	params := parseOneOfParam2(fl.Param())
+	for _, param := range params {
+		if requireCheckFieldKind(fl, param, true) {
+			return hasValue(fl)
+		}
 	}
 	return true
 }
@@ -2062,8 +2063,8 @@ func isGteField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && currentField.Type().ConvertibleTo(timeType) {
-			t := currentField.Convert(timeType).Interface().(time.Time)
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(currentField.Convert(timeType)).(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
 
 			return fieldTime.After(t) || fieldTime.Equal(t)
 		}
@@ -2106,8 +2107,8 @@ func isGtField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && currentField.Type().ConvertibleTo(timeType) {
-			t := currentField.Convert(timeType).Interface().(time.Time)
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(currentField.Convert(timeType)).(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
 
 			return fieldTime.After(t)
 		}
@@ -2162,13 +2163,13 @@ func isGte(fl FieldLevel) bool {
 
 		if field.Type().ConvertibleTo(timeType) {
 			now := time.Now().UTC()
-			t := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(field.Convert(timeType)).(time.Time)
 
 			return t.After(now) || t.Equal(now)
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isGt is the validation function for validating if the current field's value is greater than the param's value.
@@ -2210,11 +2211,11 @@ func isGt(fl FieldLevel) bool {
 	case reflect.Struct:
 
 		if field.Type().ConvertibleTo(timeType) {
-			return field.Convert(timeType).Interface().(time.Time).After(time.Now().UTC())
+			return getValue(field.Convert(timeType)).(time.Time).After(time.Now().UTC())
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // hasLengthOf is the validation function for validating if the current field's value is equal to the param's value.
@@ -2254,7 +2255,7 @@ func hasLengthOf(fl FieldLevel) bool {
 		return field.Float() == p
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // hasMinOf is the validation function for validating if the current field's value is greater than or equal to the param's value.
@@ -2290,8 +2291,8 @@ func isLteField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && currentField.Type().ConvertibleTo(timeType) {
-			t := currentField.Convert(timeType).Interface().(time.Time)
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(currentField.Convert(timeType)).(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
 
 			return fieldTime.Before(t) || fieldTime.Equal(t)
 		}
@@ -2334,8 +2335,8 @@ func isLtField(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(timeType) && currentField.Type().ConvertibleTo(timeType) {
-			t := currentField.Convert(timeType).Interface().(time.Time)
-			fieldTime := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(currentField.Convert(timeType)).(time.Time)
+			fieldTime := getValue(field.Convert(timeType)).(time.Time)
 
 			return fieldTime.Before(t)
 		}
@@ -2390,13 +2391,13 @@ func isLte(fl FieldLevel) bool {
 
 		if field.Type().ConvertibleTo(timeType) {
 			now := time.Now().UTC()
-			t := field.Convert(timeType).Interface().(time.Time)
+			t := getValue(field.Convert(timeType)).(time.Time)
 
 			return t.Before(now) || t.Equal(now)
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isLt is the validation function for validating if the current field's value is less than the param's value.
@@ -2438,11 +2439,11 @@ func isLt(fl FieldLevel) bool {
 	case reflect.Struct:
 
 		if field.Type().ConvertibleTo(timeType) {
-			return field.Convert(timeType).Interface().(time.Time).Before(time.Now().UTC())
+			return getValue(field.Convert(timeType)).(time.Time).Before(time.Now().UTC())
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // hasMaxOf is the validation function for validating if the current field's value is less than or equal to the param's value.
@@ -2612,7 +2613,7 @@ func isDir(fl FieldLevel) bool {
 		return fileInfo.IsDir()
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isDirPath is the validation function for validating if the current field's value is a valid directory.
@@ -2669,7 +2670,7 @@ func isDirPath(fl FieldLevel) bool {
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isJSON is the validation function for validating if the current field's value is a valid json string.
@@ -2684,12 +2685,12 @@ func isJSON(fl FieldLevel) bool {
 		fieldType := field.Type()
 
 		if fieldType.ConvertibleTo(byteSliceType) {
-			b := field.Convert(byteSliceType).Interface().([]byte)
+			b := getValue(field.Convert(byteSliceType)).([]byte)
 			return json.Valid(b)
 		}
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isJWT is the validation function for validating if the current field's value is a valid JWT string.
@@ -2736,7 +2737,7 @@ func isLowercase(fl FieldLevel) bool {
 		return field.String() == strings.ToLower(field.String())
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isUppercase is the validation function for validating if the current field's value is an uppercase string.
@@ -2750,7 +2751,7 @@ func isUppercase(fl FieldLevel) bool {
 		return field.String() == strings.ToUpper(field.String())
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isDatetime is the validation function for validating if the current field's value is a valid datetime string.
@@ -2764,7 +2765,7 @@ func isDatetime(fl FieldLevel) bool {
 		return err == nil
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isTimeZone is the validation function for validating if the current field's value is a valid time zone string.
@@ -2786,7 +2787,7 @@ func isTimeZone(fl FieldLevel) bool {
 		return err == nil
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isIso3166Alpha2 is the validation function for validating if the current field's value is a valid iso3166-1 alpha-2 country code.
@@ -2830,7 +2831,7 @@ func isIso3166AlphaNumeric(fl FieldLevel) bool {
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		code = int(field.Uint() % 1000)
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 
 	_, ok := iso3166_1_alpha_numeric[code]
@@ -2854,7 +2855,7 @@ func isIso3166AlphaNumericEU(fl FieldLevel) bool {
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		code = int(field.Uint() % 1000)
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 
 	_, ok := iso3166_1_alpha_numeric_eu[code]
@@ -2884,7 +2885,7 @@ func isIso4217Numeric(fl FieldLevel) bool {
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		code = int(field.Uint())
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 
 	_, ok := iso4217_numeric[code]
@@ -2900,7 +2901,7 @@ func isBCP47LanguageTag(fl FieldLevel) bool {
 		return err == nil
 	}
 
-	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	panic(fmt.Sprintf("Bad field type %s", field.Type()))
 }
 
 // isIsoBicFormat is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362
@@ -3023,7 +3024,7 @@ func hasLuhnChecksum(fl FieldLevel) bool {
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		str = strconv.FormatUint(field.Uint(), 10)
 	default:
-		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+		panic(fmt.Sprintf("Bad field type %s", field.Type()))
 	}
 	size := len(str)
 	if size < 2 { // there has to be at least one digit that carries a meaning + the checksum
diff --git a/doc.go b/doc.go
index 91bace7..23cce99 100644
--- a/doc.go
+++ b/doc.go
@@ -188,7 +188,7 @@ Same as structonly tag except that any struct level validations will not run.
 
 # Omit Empty
 
-Allows conditional validation, for example if a field is not set with
+Allows conditional validation, for example, if a field is not set with
 a value (Determined by the "required" validator) then other validation
 such as min or max won't run, but if a value is set validation will run.
 
diff --git a/errors.go b/errors.go
index 2ff5756..fd90625 100644
--- a/errors.go
+++ b/errors.go
@@ -105,22 +105,24 @@ type FieldError interface {
 	// StructNamespace returns the namespace for the field error, with the field's
 	// actual name.
 	//
-	// eq. "User.FirstName" see Namespace for comparison
+	// eg. "User.FirstName" see Namespace for comparison
 	//
 	// NOTE: this field can be blank when validating a single primitive field
 	// using validate.Field(...) as there is no way to extract its name
 	StructNamespace() string
 
-	// Field returns the fields name with the tag name taking precedence over the
+	// Field returns the field's name with the tag name taking precedence over the
 	// field's actual name.
 	//
-	// eq. JSON name "fname"
+	// `RegisterTagNameFunc` must be registered to get tag value.
+	//
+	// eg. JSON name "fname"
 	// see StructField for comparison
 	Field() string
 
 	// StructField returns the field's actual name from the struct, when able to determine.
 	//
-	// eq.  "FirstName"
+	// eg.  "FirstName"
 	// see Field for comparison
 	StructField() string
 
diff --git a/struct_level.go b/struct_level.go
index ffe295e..129b287 100644
--- a/struct_level.go
+++ b/struct_level.go
@@ -147,7 +147,7 @@ func (v *validate) ReportError(field interface{}, fieldName, structFieldName, ta
 			structNs:       v.str2,
 			fieldLen:       uint8(len(fieldName)),
 			structfieldLen: uint8(len(structFieldName)),
-			value:          fv.Interface(),
+			value:          getValue(fv),
 			param:          param,
 			kind:           kind,
 			typ:            fv.Type(),
diff --git a/translations/en/en.go b/translations/en/en.go
index 9458edc..0bf4f7f 100644
--- a/translations/en/en.go
+++ b/translations/en/en.go
@@ -1229,6 +1229,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} must be a valid ISSN number",
 			override:    false,
 		},
+		{
+			tag:         "urn_rfc2141",
+			translation: "{0} must be a valid RFC 2141 URN",
+			override:    false,
+		},
 		{
 			tag:         "uuid",
 			translation: "{0} must be a valid UUID",
diff --git a/util.go b/util.go
index fb7fa78..b1fd8cc 100644
--- a/util.go
+++ b/util.go
@@ -217,7 +217,7 @@ BEGIN:
 	panic("Invalid field namespace")
 }
 
-// asInt returns the parameter as a int64
+// asInt returns the parameter as an int64
 // or panics if it can't convert
 func asInt(param string) int64 {
 	i, err := strconv.ParseInt(param, 0, 64)
@@ -296,7 +296,7 @@ func fieldMatchesRegexByStringerValOrString(regexFn func() *regexp.Regexp, fl Fi
 	case reflect.String:
 		return regex.MatchString(fl.Field().String())
 	default:
-		if stringer, ok := fl.Field().Interface().(fmt.Stringer); ok {
+		if stringer, ok := getValue(fl.Field()).(fmt.Stringer); ok {
 			return regex.MatchString(stringer.String())
 		} else {
 			return regex.MatchString(fl.Field().String())
diff --git a/validator.go b/validator.go
index 2347d4c..995b0e1 100644
--- a/validator.go
+++ b/validator.go
@@ -260,7 +260,7 @@ OUTER:
 					return
 				}
 			default:
-				if v.fldIsPointer && field.Interface() == nil {
+				if v.fldIsPointer && getValue(field) == nil {
 					return
 				}
 			}
@@ -312,7 +312,7 @@ OUTER:
 				reusableCF := &cField{}
 
 				for _, key := range current.MapKeys() {
-					pv = fmt.Sprintf("%v", key.Interface())
+					pv = fmt.Sprintf("%v", key)
 
 					v.misc = append(v.misc[0:0], cf.name...)
 					v.misc = append(v.misc, '[')
@@ -337,6 +337,18 @@ OUTER:
 						// can be nil when just keys being validated
 						if ct.next != nil {
 							v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct.next)
+						} else {
+							// Struct fallback when map values are structs
+							val := current.MapIndex(key)
+							switch val.Kind() {
+							case reflect.Ptr:
+								if val.Elem().Kind() == reflect.Struct {
+									// Dive into the struct so its own tags run
+									v.traverseField(ctx, parent, val, ns, structNs, reusableCF, nil)
+								}
+							case reflect.Struct:
+								v.traverseField(ctx, parent, val, ns, structNs, reusableCF, nil)
+							}
 						}
 					} else {
 						v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct)


Test patch:
diff --git a/translations/en/en_test.go b/translations/en/en_test.go
index 44ce09a..d2a0346 100644
--- a/translations/en/en_test.go
+++ b/translations/en/en_test.go
@@ -129,6 +129,7 @@ func TestTranslations(t *testing.T) {
 		ISBN10             string            `validate:"isbn10"`
 		ISBN13             string            `validate:"isbn13"`
 		ISSN               string            `validate:"issn"`
+		URN                string            `validate:"urn_rfc2141"`
 		UUID               string            `validate:"uuid"`
 		UUID3              string            `validate:"uuid3"`
 		UUID4              string            `validate:"uuid4"`
@@ -406,6 +407,10 @@ func TestTranslations(t *testing.T) {
 			ns:       "Test.ISSN",
 			expected: "ISSN must be a valid ISSN number",
 		},
+		{
+			ns:       "Test.URN",
+			expected: "URN must be a valid RFC 2141 URN",
+		},
 		{
 			ns:       "Test.Excludes",
 			expected: "Excludes cannot contain the text 'text'",
diff --git a/validator_test.go b/validator_test.go
index 513218d..2fcb3ff 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -6010,7 +6010,7 @@ func TestImageValidation(t *testing.T) {
 	}
 
 	PanicMatches(t, func() {
-		_ = validate.Var(6, "file")
+		_ = validate.Var(6, "image")
 	}, "Bad field type int")
 }
 
@@ -8255,7 +8255,9 @@ func TestUrl(t *testing.T) {
 		{"file:///c:/Windows/file.txt", true},
 		{"file://localhost/path/to/file.txt", true},
 		{"file://localhost/c:/WINDOWS/file.txt", true},
-		{"file://", true},
+		{"file:", false},
+		{"file:/", false},
+		{"file://", false},
 		{"file:////remotehost/path/file.txt", true},
 	}
 
@@ -11865,6 +11867,32 @@ func TestRequiredWithout(t *testing.T) {
 
 	errs = validate.Struct(&test3)
 	Equal(t, errs, nil)
+
+	test4 := struct {
+		Field1 string `validate:"required_without=Field2 Field3,omitempty,min=1" json:"field_1"`
+		Field2 string `json:"field_2"`
+		Field3 string `json:"field_3"`
+	}{
+		Field1: "test",
+	}
+
+	errs = validate.Struct(&test4)
+	Equal(t, errs, nil)
+
+	test5 := struct {
+		Field1 string `validate:"required_without=Field2 Field3,omitempty,min=1" json:"field_1"`
+		Field2 string `json:"field_2"`
+		Field3 string `json:"field_3"`
+	}{
+		Field3: "test",
+	}
+
+	errs = validate.Struct(&test5)
+	NotEqual(t, errs, nil)
+
+	ve = errs.(ValidationErrors)
+	Equal(t, len(ve), 1)
+	AssertError(t, errs, "Field1", "Field1", "Field1", "Field1", "required_without")
 }
 
 func TestRequiredWithoutAll(t *testing.T) {
@@ -14137,6 +14165,16 @@ func TestPrivateFieldsStruct(t *testing.T) {
 			}{},
 			errorNum: 2,
 		},
+		{
+			stct: &struct {
+				f1 map[string]string `validate:"required,dive,required"`
+				f2 *int              `validate:"omitnil,min=2"`
+			}{
+				f1: map[string]string{"key": ""},
+				f2: intPtr(1),
+			},
+			errorNum: 2,
+		},
 	}
 
 	validate := New(WithPrivateFieldValidation())
@@ -14148,6 +14186,12 @@ func TestPrivateFieldsStruct(t *testing.T) {
 		errs := err.(ValidationErrors)
 		Equal(t, len(errs), tc.errorNum)
 	}
+
+	stct := &struct {
+		f1 int `validate:"uri"`
+	}{}
+
+	PanicMatches(t, func() { _ = validate.Struct(stct) }, "Bad field type int")
 }
 
 type NotRed struct {
@@ -14293,3 +14337,641 @@ func TestValidateFn(t *testing.T) {
 		Equal(t, fe.Tag(), "validateFn")
 	})
 }
+func TestMapStructBasicValidation(t *testing.T) {
+	// Tests basic validation of a map with struct values
+	type Inner struct {
+		Value string `validate:"max=5"`
+	}
+	type Outer struct {
+		Data map[string]Inner `validate:"dive"`
+	}
+	obj := Outer{
+		Data: map[string]Inner{
+			"key1": {Value: "exceeds"}, // Should fail because Value is longer than 5 characters
+		},
+	}
+	validate := New()
+	errs := validate.Struct(obj)
+	NotEqual(t, errs, nil)
+}
+func TestMapStructPointerValidation(t *testing.T) {
+	// Tests validation of a map with pointer to struct values
+	type Inner struct {
+		Count int `validate:"gt=10"`
+	}
+	type Outer struct {
+		Items map[string]*Inner `validate:"dive"`
+	}
+	obj := Outer{
+		Items: map[string]*Inner{
+			"a": {Count: 5},
+		},
+	}
+	validate := New()
+	errs := validate.Struct(obj)
+	NotEqual(t, errs, nil)
+}
+func TestMapStructWithKeyValidationOnly(t *testing.T) {
+	// Tests validation of a map with struct values, focusing on key validation
+	type Inner struct {
+		Name string `validate:"required"`
+	}
+	type Outer struct {
+		Things map[string]Inner `validate:"dive,keys,min=3,endkeys"`
+	}
+	obj := Outer{
+		Things: map[string]Inner{
+			"ab": {Name: "valid"}, //Should fail because key is too short
+		},
+	}
+	validate := New()
+	errs := validate.Struct(obj)
+	NotEqual(t, errs, nil)
+}
+func TestMapStructWithKeyAndValueValidation(t *testing.T) {
+	// Tests validation of a map with struct values, validating both keys and values
+	type Inner struct {
+		Name string `validate:"min=3"`
+	}
+	type Outer struct {
+		Stuff map[string]Inner `validate:"dive,keys,min=2,endkeys"`
+	}
+	obj := Outer{
+		Stuff: map[string]Inner{
+			"ok":  {Name: "xy"},
+			"bad": {Name: "valid"},
+		},
+	}
+	validate := New()
+	errs := validate.Struct(obj)
+	NotEqual(t, errs, nil)
+}
+func TestMapPointerStructWithNilValue(t *testing.T) {
+	// Tests validation of a map with pointer to struct values where value is nil
+	type Inner struct {
+		Count int `validate:"min=1"`
+	}
+	type Outer struct {
+		Items map[string]*Inner `validate:"dive"`
+	}
+	obj := Outer{
+		Items: map[string]*Inner{
+			"x": nil,
+		},
+	}
+	validate := New()
+	errs := validate.Struct(obj)
+	Equal(t, errs, nil)
+}
+func TestThreeLevelNestedStructs(t *testing.T) {
+	// Tests validation of three levels of nested structs with map keys and values
+	type Level3 struct {
+		Code string `validate:"len=3"`
+	}
+
+	type Level2 struct {
+		Items map[string]Level3 `validate:"dive,keys,required,endkeys"`
+	}
+
+	type Level1 struct {
+		Levels map[string]Level2 `validate:"dive,keys,required,endkeys"`
+	}
+
+	validate := New()
+
+	// Valid case: all Level3.Code are exactly 3 chars
+	valid := Level1{Levels: map[string]Level2{
+		"first": {Items: map[string]Level3{
+			"item1": {Code: "abc"},
+			"item2": {Code: "xyz"},
+		},
+		},
+	},
+	}
+
+	errs := validate.Struct(valid)
+	Equal(t, errs, nil)
+
+	// Invalid case: one Level3.Code is wrong length
+	invalid := Level1{Levels: map[string]Level2{
+		"first": {Items: map[string]Level3{
+			"item1": {Code: "abcd"}, // Should fail here because length is 4
+			"item2": {Code: "xyz"},
+		},
+		},
+	},
+	}
+
+	errs = validate.Struct(invalid)
+	NotEqual(t, errs, nil)
+}
+func TestMapStructFallbackWithKeysOnly(t *testing.T) {
+	// Tests fallback behavior when validating map keys and struct values
+	type Inner struct {
+		Value string `validate:"max=3"`
+	}
+	type Outer struct {
+		Data map[string]Inner `validate:"dive,keys,max=2,endkeys"`
+	}
+	validate := New()
+
+	// Key too long: should fail on key before fallback
+	obj1 := Outer{Data: map[string]Inner{"toolong": {Value: "ok"}}}
+	errs := validate.Struct(obj1)
+	NotEqual(t, errs, nil)
+
+	// Key OK, value too long: should fallback and fail on Inner.Value
+	obj2 := Outer{Data: map[string]Inner{"ok": {Value: "toolong"}}}
+	errs = validate.Struct(obj2)
+	NotEqual(t, errs, nil)
+
+	// Both key and value OK: should pass
+	obj3 := Outer{Data: map[string]Inner{"ok": {Value: "abc"}}}
+	errs = validate.Struct(obj3)
+	Equal(t, errs, nil)
+}
+
+func TestMapPointerStructFallback(t *testing.T) {
+	// Tests fallback behavior when validating map keys and pointer to struct values
+	type Inner struct {
+		Count int `validate:"gt=0"`
+	}
+	type Outer struct {
+		Data map[string]*Inner `validate:"dive,keys,max=3,endkeys"`
+	}
+	validate := New()
+
+	// Key OK, pointer is nil: no fallback error
+	obj1 := Outer{Data: map[string]*Inner{"ok": nil}}
+	errs := validate.Struct(obj1)
+	Equal(t, errs, nil)
+
+	// Key OK, pointer non-nil but field invalid: fallback should validate Inner.Count
+	obj2 := Outer{Data: map[string]*Inner{"ok": {Count: 0}}}
+	errs = validate.Struct(obj2)
+	NotEqual(t, errs, nil)
+
+	// Key OK, pointer non-nil and valid: should pass
+	obj3 := Outer{Data: map[string]*Inner{"ok": {Count: 5}}}
+	errs = validate.Struct(obj3)
+	Equal(t, errs, nil)
+}
+
+func TestMapNonStructValueSkipsFallback(t *testing.T) {
+	// Tests that fallback is skipped for non-struct values in maps
+	type Outer struct {
+		Data map[string]int `validate:"dive,keys,min=1,endkeys"`
+	}
+	validate := New()
+
+	// Key OK, value is primitive: no fallback needed, should pass
+	obj1 := Outer{Data: map[string]int{"a": 0}}
+	errs := validate.Struct(obj1)
+	Equal(t, errs, nil)
+
+	// Key too short: should fail on key
+	obj2 := Outer{Data: map[string]int{"": 0}}
+	errs = validate.Struct(obj2)
+	NotEqual(t, errs, nil)
+}
+
+func TestMapSliceValueNoFallback(t *testing.T) {
+	// Tests that fallback is skipped for slice values in maps
+	type Outer struct {
+		Data map[string][]string `validate:"dive,keys,max=1,endkeys"`
+	}
+	validate := New()
+
+	// Key OK, value is slice: skip fallback, should pass
+	obj1 := Outer{Data: map[string][]string{"a": {"x", "y"}}}
+	errs := validate.Struct(obj1)
+	Equal(t, errs, nil)
+
+	// Key too long: should fail on key
+	obj2 := Outer{Data: map[string][]string{"ab": {"x"}}}
+	errs = validate.Struct(obj2)
+	NotEqual(t, errs, nil)
+}
+func TestMapEmptyStructValueNoError(t *testing.T) {
+	// Tests that empty struct values in maps do not trigger validation errors
+	type Inner struct{} // no validation tags
+
+	type Outer struct {
+		Data map[string]Inner `validate:"dive,keys,max=3,endkeys"`
+	}
+	validate := New()
+
+	// Key OK, value is empty struct: no fields to validate, should pass
+	obj1 := Outer{Data: map[string]Inner{"ok": {}}}
+	errs := validate.Struct(obj1)
+	Equal(t, errs, nil)
+
+	// Key too long: should fail on key, skip struct
+	obj2 := Outer{Data: map[string]Inner{"toolong": {}}}
+	errs = validate.Struct(obj2)
+	NotEqual(t, errs, nil)
+}
+
+func TestMapNestedEmptyStructs(t *testing.T) {
+	// Tests that nested empty structs in maps validate correctly
+	type Level3 struct{}
+	type Level2 struct {
+		Items map[string]Level3 `validate:"dive,keys,max=2,endkeys"`
+	}
+	type Level1 struct {
+		Levels map[string]Level2 `validate:"dive,keys,max=2,endkeys"`
+	}
+
+	validate := New()
+
+	// All keys within max=2, and inner structs are empty → should pass
+	obj := Level1{Levels: map[string]Level2{
+		"a": {Items: map[string]Level3{"b": {}}},
+	}}
+	errs := validate.Struct(obj)
+	Equal(t, errs, nil)
+
+	// Top-level key too long: should fail on Level1 key, skip deeper
+	obj2 := Level1{Levels: map[string]Level2{
+		"too": {Items: map[string]Level3{"b": {}}}, // "too" length=3 > max=2
+	}}
+	errs = validate.Struct(obj2)
+	NotEqual(t, errs, nil)
+}
+
+func TestMapEmptyValueMap(t *testing.T) {
+	// Tests that an empty map with struct values does not trigger validation errors
+	type Inner struct {
+		Value string `validate:"required"`
+	}
+
+	type Outer struct {
+		Data map[string]Inner `validate:"dive,keys,max=3,endkeys"`
+	}
+	validate := New()
+
+	// Empty map: nothing to validate, should pass
+	obj := Outer{Data: map[string]Inner{}}
+	errs := validate.Struct(obj)
+	Equal(t, errs, nil)
+}
+
+func TestMapEmptyPointerStructValueNoError(t *testing.T) {
+	// Tests that empty pointer struct values in maps do not trigger validation errors
+	type Inner struct{}
+
+	type Outer struct {
+		Data map[string]*Inner `validate:"dive,keys,max=3,endkeys"`
+	}
+	validate := New()
+
+	// Key OK, pointer is non-nil empty struct: should pass
+	obj1 := Outer{Data: map[string]*Inner{"ok": {}}}
+	errs := validate.Struct(obj1)
+	Equal(t, errs, nil)
+
+	// Key OK, pointer is nil: no tags on Inner, so should pass
+	obj2 := Outer{Data: map[string]*Inner{"ok": nil}}
+	errs = validate.Struct(obj2)
+	Equal(t, errs, nil)
+}
+func TestOmitZeroWithSlices(t *testing.T) {
+	// Tests the behavior of omitempty and omitzero with slices
+	type OmitEmptyExample struct {
+		Data []string `validate:"omitempty,min=2"`
+	}
+
+	type OmitZeroExample struct {
+		Data []string `validate:"omitzero,min=2"`
+	}
+
+	validate := New()
+
+	t.Run("nil slices", func(t *testing.T) {
+		// Test 1: nil slices
+		test1Empty := OmitEmptyExample{Data: nil}
+		test1Zero := OmitZeroExample{Data: nil}
+
+		err1 := validate.Struct(test1Empty)
+		err2 := validate.Struct(test1Zero)
+
+		Equal(t, err1, nil) // No error (skipped)
+		Equal(t, err2, nil) // No error (skipped)
+	})
+
+	t.Run("empty but non-nil slices", func(t *testing.T) {
+		// Test 2: empty but non-nil slices
+		test2Empty := OmitEmptyExample{Data: []string{}}
+		test2Zero := OmitZeroExample{Data: []string{}}
+
+		err1 := validate.Struct(test2Empty)
+		err2 := validate.Struct(test2Zero)
+
+		NotEqual(t, err1, nil) // Error (min=2)
+		Equal(t, err2, nil)    // No error (should be skipped)
+	})
+
+	t.Run("single item slices", func(t *testing.T) {
+		// Test 3: single item slices (still not meeting min=2)
+		test3Empty := OmitEmptyExample{Data: []string{"one"}}
+		test3Zero := OmitZeroExample{Data: []string{"one"}}
+
+		err1 := validate.Struct(test3Empty)
+		err2 := validate.Struct(test3Zero)
+
+		NotEqual(t, err1, nil) // Error (min=2)
+		NotEqual(t, err2, nil) // Error (min=2)
+	})
+
+	t.Run("valid slices", func(t *testing.T) {
+		// Test 4: valid slices (min=2 satisfied)
+		test4Empty := OmitEmptyExample{Data: []string{"one", "two"}}
+		test4Zero := OmitZeroExample{Data: []string{"one", "two"}}
+
+		err1 := validate.Struct(test4Empty)
+		err2 := validate.Struct(test4Zero)
+
+		Equal(t, err1, nil) // No error
+		Equal(t, err2, nil) // No error
+	})
+}
+func TestOmitZeroWithMaps(t *testing.T) {
+	// Tests the behavior of omitempty and omitzero with maps
+	type OmitEmptyExample struct {
+		Data map[string]string `validate:"omitempty,min=2"`
+	}
+
+	type OmitZeroExample struct {
+		Data map[string]string `validate:"omitzero,min=2"`
+	}
+
+	validate := New()
+
+	t.Run("nil maps", func(t *testing.T) {
+		// Test 1: nil maps
+		test1Empty := OmitEmptyExample{Data: nil}
+		test1Zero := OmitZeroExample{Data: nil}
+
+		err1 := validate.Struct(test1Empty)
+		err2 := validate.Struct(test1Zero)
+
+		Equal(t, err1, nil) // No error (skipped)
+		Equal(t, err2, nil) // No error (skipped)
+	})
+
+	t.Run("empty but non-nil maps", func(t *testing.T) {
+		// Test 2: empty but non-nil maps
+		test2Empty := OmitEmptyExample{Data: map[string]string{}}
+		test2Zero := OmitZeroExample{Data: map[string]string{}}
+
+		err1 := validate.Struct(test2Empty)
+		err2 := validate.Struct(test2Zero)
+
+		NotEqual(t, err1, nil) // Error (min=2)
+		Equal(t, err2, nil)    // No error (should be skipped)
+	})
+
+	t.Run("single item maps", func(t *testing.T) {
+		// Test 3: single item maps (still not meeting min=2)
+		test3Empty := OmitEmptyExample{Data: map[string]string{"key1": "value1"}}
+		test3Zero := OmitZeroExample{Data: map[string]string{"key1": "value1"}}
+
+		err1 := validate.Struct(test3Empty)
+		err2 := validate.Struct(test3Zero)
+
+		NotEqual(t, err1, nil) // Error (min=2)
+		NotEqual(t, err2, nil) // Error (min=2)
+	})
+
+	t.Run("valid maps", func(t *testing.T) {
+		// Test 4: valid maps (min=2 satisfied)
+		test4Empty := OmitEmptyExample{Data: map[string]string{"key1": "value1", "key2": "value2"}}
+		test4Zero := OmitZeroExample{Data: map[string]string{"key1": "value1", "key2": "value2"}}
+
+		err1 := validate.Struct(test4Empty)
+		err2 := validate.Struct(test4Zero)
+
+		Equal(t, err1, nil) // No error
+		Equal(t, err2, nil) // No error
+	})
+}
+func TestRequiredIfWithNilBytesSlice(t *testing.T) {
+	// Tests the behavior of required_if with nil byte slices
+	type TextOrBytes struct {
+		Text  string `validate:"required_if=Bytes nil"`
+		Bytes []byte `validate:"required_if=Text nil"`
+	}
+
+	validate := New(WithRequiredStructEnabled())
+
+	t.Run("Text empty, Bytes empty but not nil", func(t *testing.T) {
+		// Test 1: Text empty, Bytes empty but not nil
+		obj := TextOrBytes{
+			Text:  "",
+			Bytes: []byte{},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Bytes is not nil, just empty
+	})
+
+	t.Run("Text empty, Bytes nil", func(t *testing.T) {
+		// Test 2: Text empty, Bytes nil
+		obj := TextOrBytes{
+			Text:  "",
+			Bytes: nil,
+		}
+		err := validate.Struct(obj)
+
+		NotEqual(t, err, nil) // Error - Text is required when Bytes is nil
+	})
+
+	t.Run("Text populated, Bytes nil", func(t *testing.T) {
+		// Test 3: Text populated, Bytes nil
+		obj := TextOrBytes{
+			Text:  "Hello",
+			Bytes: nil,
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Text has value when Bytes is nil
+	})
+
+	t.Run("Text populated, Bytes empty but not nil", func(t *testing.T) {
+		// Test 4: Text populated, Bytes empty but not nil
+		obj := TextOrBytes{
+			Text:  "Hello",
+			Bytes: []byte{},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Neither field is nil
+	})
+
+	t.Run("Text empty, Bytes populated", func(t *testing.T) {
+		// Test 5: Text empty, Bytes populated
+		obj := TextOrBytes{
+			Text:  "",
+			Bytes: []byte("World"),
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Bytes has value when Text is empty
+	})
+
+	t.Run("Both fields populated", func(t *testing.T) {
+		// Test 6: Both fields populated
+		obj := TextOrBytes{
+			Text:  "Hello",
+			Bytes: []byte("World"),
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Both fields have values
+	})
+}
+func TestRequiredIfWithMaps(t *testing.T) {
+	// Tests the behavior of required_if with maps
+	type TextOrMap struct {
+		Text string                 `validate:"required_if=Data nil"`
+		Data map[string]interface{} `validate:"required_if=Text nil"`
+	}
+
+	validate := New(WithRequiredStructEnabled())
+
+	t.Run("Text empty, Map nil", func(t *testing.T) {
+		// Test 1: Text empty, Map nil
+		obj := TextOrMap{
+			Text: "",
+			Data: nil,
+		}
+		err := validate.Struct(obj)
+
+		NotEqual(t, err, nil) // Error - Text is required when Data is nil
+	})
+
+	t.Run("Text populated, Map nil", func(t *testing.T) {
+		// Test 2: Text populated, Map nil
+		obj := TextOrMap{
+			Text: "Hello",
+			Data: nil,
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Text has value when Data is nil
+	})
+
+	t.Run("Text empty, Map empty but not nil", func(t *testing.T) {
+		// Test 3: Text empty, Map empty but not nil
+		obj := TextOrMap{
+			Text: "",
+			Data: map[string]interface{}{},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Data is not nil, just empty
+	})
+
+	t.Run("Text populated, Map empty but not nil", func(t *testing.T) {
+		// Test 4: Text populated, Map empty but not nil
+		obj := TextOrMap{
+			Text: "Hello",
+			Data: map[string]interface{}{},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Neither field is nil
+	})
+
+	t.Run("Text empty, Map populated", func(t *testing.T) {
+		// Test 5: Text empty, Map populated
+		obj := TextOrMap{
+			Text: "",
+			Data: map[string]interface{}{"key": "value"},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Data has value when Text is empty
+	})
+
+	t.Run("Both fields populated", func(t *testing.T) {
+		// Test 6: Both fields populated
+		obj := TextOrMap{
+			Text: "Hello",
+			Data: map[string]interface{}{"key": "value"},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Both fields have values
+	})
+}
+func TestRequiredIfWithArrays(t *testing.T) {
+	// Tests the behavior of required_if with arrays
+	type TextOrArray struct {
+		Text string    `validate:"required_if=Data 3"`   // Check if array length is 3
+		Data [3]string `validate:"required_if=Text nil"` // Check if Text is empty
+	}
+
+	// Also test with a different length to verify behavior
+	type TextOrSmallArray struct {
+		Text string    `validate:"required_if=Data 2"`   // Check if array length is 2 (which it never will be for [3]string)
+		Data [3]string `validate:"required_if=Text nil"` // Check if Text is empty
+	}
+
+	validate := New(WithRequiredStructEnabled())
+
+	t.Run("Array length always matches declared size", func(t *testing.T) {
+		// Test 1: Text empty, Array has length 3
+		obj := TextOrArray{
+			Text: "",
+			Data: [3]string{}, // Zero values but length is 3
+		}
+		err := validate.Struct(obj)
+
+		NotEqual(t, err, nil) // Error - Text is required when Data length equals 3
+	})
+
+	t.Run("Text populated, Array always has declared length", func(t *testing.T) {
+		// Test 2: Text populated, Array has length 3
+		obj := TextOrArray{
+			Text: "Hello",
+			Data: [3]string{},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Text has value
+	})
+
+	t.Run("Length check that never matches doesn't trigger validation", func(t *testing.T) {
+		// Test 3: Text empty, Array has length 3 but we check for length 2
+		obj := TextOrSmallArray{
+			Text: "",
+			Data: [3]string{},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Data length never equals 2
+	})
+
+	t.Run("Array values don't affect length check", func(t *testing.T) {
+		// Test 4: Text empty, Array partially populated but still length 3
+		obj := TextOrArray{
+			Text: "",
+			Data: [3]string{"One", "Two", ""},
+		}
+		err := validate.Struct(obj)
+
+		NotEqual(t, err, nil) // Error - Text is required when Data length equals 3
+	})
+
+	t.Run("Array values with Text populated passes validation", func(t *testing.T) {
+		// Test 5: Text populated, Array with values
+		obj := TextOrArray{
+			Text: "Hello",
+			Data: [3]string{"One", "Two", "Three"},
+		}
+		err := validate.Struct(obj)
+
+		Equal(t, err, nil) // No error - Text has value
+	})
+}