Repository: go-playground/validator
Pull request #1048 (2022-12-27 14:09:44)

feat(unique): Add support for struct memember validation
## Fixes Or Enhances

Issue #1034

This allows validating that two struct members are unique. This has
been documented as a feature but has never been implemented.


**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/55477388/badge)](https://coveralls.io/builds/55477388)

Coverage: 74.181% (-0.001%) from 74.182% when pulling **0a12a12d998313ae9a5814f58d0116e2ce7f326d on zemzale:unique-struct** into **c7e0172e0fd176bdc521afb5186818a7db6b77ac on go-playground:master**.


Patch:
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 8c07a64..041b304 100644
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -8,7 +8,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.17.x, 1.18.x]
+        go-version: [1.19.x]
         os: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.os }}
     steps:
@@ -32,7 +32,7 @@ jobs:
         run: go test -race -covermode=atomic -coverprofile="profile.cov" ./...
 
       - name: Send Coverage
-        if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.18.x'
+        if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.19.x'
         uses: shogo82148/actions-goveralls@v1
         with:
           path-to-profile: profile.cov
@@ -43,9 +43,9 @@ jobs:
     steps:
       - uses: actions/setup-go@v3
         with:
-          go-version: 1.18.x
+          go-version: 1.19.x
       - uses: actions/checkout@v3
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v3
         with:
-          version: v1.45.2
+          version: v1.50.1
diff --git a/.gitignore b/.gitignore
index 6e43fac..2410a91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,4 @@ _testmain.go
 *.txt
 cover.html
 README.html
+.idea
diff --git a/README.md b/README.md
index 9d0a79e..284092b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 Package validator
 =================
 <img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v9/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.11.1-green.svg)
+![Project status](https://img.shields.io/badge/version-10.11.2-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)
 [![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)
@@ -73,8 +73,8 @@ Baked-in Validations
 | - | - |
 | eqcsfield | Field Equals Another Field (relative)|
 | eqfield | Field Equals Another Field |
-| fieldcontains | NOT DOCUMENTED IN doc.go |
-| fieldexcludes | NOT DOCUMENTED IN doc.go |
+| fieldcontains | Check the indicated characters are present in the Field |
+| fieldexcludes | Check the indicated characters are not present in the field |
 | gtcsfield | Field Greater Than Another Relative Field |
 | gtecsfield | Field Greater Than or Equal To Another Relative Field |
 | gtefield | Field Greater Than or Equal To Another Field |
@@ -114,6 +114,7 @@ Baked-in Validations
 | unix_addr | Unix domain socket end point Address |
 | uri | URI String |
 | url | URL String |
+| http_url | HTTP URL String |
 | url_encoded | URL Encoded |
 | urn_rfc2141 | Urn RFC 2141 String |
 
@@ -137,7 +138,7 @@ Baked-in Validations
 | excludesrune | Excludes Rune |
 | lowercase | Lowercase |
 | multibyte | Multi-Byte Characters |
-| number | NOT DOCUMENTED IN doc.go |
+| number | Number |
 | numeric | Numeric |
 | printascii | Printable ASCII |
 | startsnotwith | Starts Not With |
@@ -154,6 +155,7 @@ Baked-in Validations
 | btc_addr | Bitcoin Address |
 | btc_addr_bech32 | Bitcoin Bech32 Address (segwit) |
 | credit_card | Credit Card Number |
+| cron | Cron |
 | datetime | Datetime |
 | e164 | e164 formatted phone number |
 | email | E-mail String
@@ -207,11 +209,13 @@ Baked-in Validations
 | Tag | Description |
 | - | - |
 | eq | Equals |
+| eq_ignore_case | Equals ignoring case |
 | gt | Greater than|
 | gte | Greater than or equal |
 | lt | Less Than |
 | lte | Less Than or Equal |
 | ne | Not Equal |
+| ne_ignore_case | Not Equal ignoring case |
 
 ### Other:
 | Tag | Description |
diff --git a/baked_in.go b/baked_in.go
index c9b1db4..1e6e00d 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -86,7 +86,9 @@ var (
 		"min":                           hasMinOf,
 		"max":                           hasMaxOf,
 		"eq":                            isEq,
+		"eq_ignore_case":                isEqIgnoreCase,
 		"ne":                            isNe,
+		"ne_ignore_case":                isNeIgnoreCase,
 		"lt":                            isLt,
 		"lte":                           isLte,
 		"gt":                            isGt,
@@ -121,6 +123,7 @@ var (
 		"e164":                          isE164,
 		"email":                         isEmail,
 		"url":                           isURL,
+		"http_url":                      isHttpURL,
 		"uri":                           isURI,
 		"urn_rfc2141":                   isUrnRFC2141, // RFC 2141
 		"file":                          isFile,
@@ -140,6 +143,7 @@ var (
 		"isbn10":                        isISBN10,
 		"isbn13":                        isISBN13,
 		"eth_addr":                      isEthereumAddress,
+		"eth_addr_checksum":             isEthereumAddressChecksum,
 		"btc_addr":                      isBitcoinAddress,
 		"btc_addr_bech32":               isBitcoinBech32Address,
 		"uuid":                          isUUID,
@@ -214,6 +218,7 @@ var (
 		"semver":                        isSemverFormat,
 		"dns_rfc1035_label":             isDnsRFC1035LabelFormat,
 		"credit_card":                   isCreditCard,
+		"cron":                          isCron,
 	}
 )
 
@@ -307,18 +312,42 @@ func isUnique(fl FieldLevel) bool {
 		}
 
 		m := reflect.MakeMap(reflect.MapOf(sfTyp, v.Type()))
+		var fieldlen int
 		for i := 0; i < field.Len(); i++ {
-			m.SetMapIndex(reflect.Indirect(reflect.Indirect(field.Index(i)).FieldByName(param)), v)
+			key := reflect.Indirect(reflect.Indirect(field.Index(i)).FieldByName(param))
+			if key.IsValid() {
+				fieldlen++
+				m.SetMapIndex(key, v)
+			}
 		}
-		return field.Len() == m.Len()
+		return fieldlen == m.Len()
 	case reflect.Map:
-		m := reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
+		var m reflect.Value
+		if field.Type().Elem().Kind() == reflect.Ptr {
+			m = reflect.MakeMap(reflect.MapOf(field.Type().Elem().Elem(), v.Type()))
+		} else {
+			m = reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
+		}
 
 		for _, k := range field.MapKeys() {
-			m.SetMapIndex(field.MapIndex(k), v)
+			m.SetMapIndex(reflect.Indirect(field.MapIndex(k)), v)
 		}
+
 		return field.Len() == m.Len()
 	default:
+		if parent := fl.Parent(); parent.Kind() == reflect.Struct {
+			uniqueField := parent.FieldByName(param)
+			if uniqueField == reflect.ValueOf(nil) {
+				panic(fmt.Sprintf("Bad field name provided %s", param))
+			}
+
+			if uniqueField.Kind() != field.Kind() {
+				panic(fmt.Sprintf("Bad field type %T:%T", field.Interface(), uniqueField.Interface()))
+			}
+
+			return field.Interface() != uniqueField.Interface()
+		}
+
 		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 	}
 }
@@ -613,14 +642,16 @@ func isISBN10(fl FieldLevel) bool {
 func isEthereumAddress(fl FieldLevel) bool {
 	address := fl.Field().String()
 
+	return ethAddressRegex.MatchString(address)
+}
+
+// isEthereumAddressChecksum is the validation function for validating if the field's value is a valid checksumed Ethereum address.
+func isEthereumAddressChecksum(fl FieldLevel) bool {
+	address := fl.Field().String()
+
 	if !ethAddressRegex.MatchString(address) {
 		return false
 	}
-
-	if ethAddressRegexUpper.MatchString(address) || ethAddressRegexLower.MatchString(address) {
-		return true
-	}
-
 	// Checksum validation. Reference: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
 	address = address[2:] // Skip "0x" prefix.
 	h := sha3.NewLegacyKeccak256()
@@ -889,6 +920,12 @@ func isNe(fl FieldLevel) bool {
 	return !isEq(fl)
 }
 
+// isNe is the validation function for validating that the field's string value does not equal the
+// provided param value. The comparison is case-insensitive
+func isNeIgnoreCase(fl FieldLevel) bool {
+	return !isEqIgnoreCase(fl)
+}
+
 // isLteCrossStructField is the validation function for validating if the current field's value is less than or equal to the field, within a separate struct, specified by the param's value.
 func isLteCrossStructField(fl FieldLevel) bool {
 	field := fl.Field()
@@ -1260,6 +1297,22 @@ func isEq(fl FieldLevel) bool {
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
 
+// isEqIgnoreCase is the validation function for validating if the current field's string value is
+// equal to the param's value.
+// The comparison is case-insensitive.
+func isEqIgnoreCase(fl FieldLevel) bool {
+	field := fl.Field()
+	param := fl.Param()
+
+	switch field.Kind() {
+
+	case reflect.String:
+		return strings.EqualFold(field.String(), param)
+	}
+
+	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+}
+
 // isPostcodeByIso3166Alpha2 validates by value which is country code in iso 3166 alpha 2
 // example: `postcode_iso3166_alpha2=US`
 func isPostcodeByIso3166Alpha2(fl FieldLevel) bool {
@@ -1370,6 +1423,23 @@ func isURL(fl FieldLevel) bool {
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
 
+// isHttpURL is the validation function for validating if the current field's value is a valid HTTP(s) URL.
+func isHttpURL(fl FieldLevel) bool {
+	if !isURL(fl) {
+		return false
+	}
+
+	field := fl.Field()
+	switch field.Kind() {
+	case reflect.String:
+
+		s := strings.ToLower(field.String())
+		return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
+	}
+
+	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+}
+
 // isUrnRFC2141 is the validation function for validating if the current field's value is a valid URN as per RFC 2141.
 func isUrnRFC2141(fl FieldLevel) bool {
 	field := fl.Field()
@@ -1539,7 +1609,9 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
 }
 
 // requireCheckFieldValue is a func for check field value
-func requireCheckFieldValue(fl FieldLevel, param string, value string, defaultNotFoundValue bool) bool {
+func requireCheckFieldValue(
+	fl FieldLevel, param string, value string, defaultNotFoundValue bool,
+) bool {
 	field, kind, _, found := fl.GetStructFieldOKAdvanced2(fl.Parent(), param)
 	if !found {
 		return defaultNotFoundValue
@@ -1623,10 +1695,10 @@ func excludedUnless(fl FieldLevel) bool {
 	}
 	for i := 0; i < len(params); i += 2 {
 		if !requireCheckFieldValue(fl, params[i], params[i+1], false) {
-			return true
+			return !hasValue(fl)
 		}
 	}
-	return !hasValue(fl)
+	return true
 }
 
 // excludedWith is the validation function
@@ -2316,7 +2388,9 @@ func isHostnamePort(fl FieldLevel) bool {
 		return false
 	}
 	// Port must be a iny <= 65535.
-	if portNum, err := strconv.ParseInt(port, 10, 32); err != nil || portNum > 65535 || portNum < 1 {
+	if portNum, err := strconv.ParseInt(
+		port, 10, 32,
+	); err != nil || portNum > 65535 || portNum < 1 {
 		return false
 	}
 
@@ -2524,3 +2598,9 @@ func isCreditCard(fl FieldLevel) bool {
 	}
 	return (sum % 10) == 0
 }
+
+// isCron is the validation function for validating if the current field's value is a valid cron expression
+func isCron(fl FieldLevel) bool {
+	cronString := fl.Field().String()
+	return cronRegex.MatchString(cronString)
+}
diff --git a/doc.go b/doc.go
index 7341c67..ba4639c 100644
--- a/doc.go
+++ b/doc.go
@@ -232,16 +232,16 @@ require another 'keys' and 'endkeys' tag. These tags are only valid for maps.
 
 Example #1
 
-	map[string]string with validation tag "gt=0,dive,keys,eg=1|eq=2,endkeys,required"
+	map[string]string with validation tag "gt=0,dive,keys,eq=1|eq=2,endkeys,required"
 	// gt=0 will be applied to the map itself
-	// eg=1|eq=2 will be applied to the map keys
+	// eq=1|eq=2 will be applied to the map keys
 	// required will be applied to map values
 
 Example #2
 
 	map[[2]string]string with validation tag "gt=0,dive,keys,dive,eq=1|eq=2,endkeys,required"
 	// gt=0 will be applied to the map itself
-	// eg=1|eq=2 will be applied to each array element in the the map keys
+	// eq=1|eq=2 will be applied to each array element in the the map keys
 	// required will be applied to map values
 
 Required
@@ -1323,6 +1323,12 @@ This validates that a string value contains a valid credit card number using Luh
 
 	Usage: credit_card
 
+Cron
+
+This validates that a string value contains a valid cron expression.
+
+	Usage: cron
+
 Alias Validators and Tags
 
 NOTE: When returning an error, the tag returned in "FieldError" will be
diff --git a/go.mod b/go.mod
index e281ec0..aa7527d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,19 +1,14 @@
 module github.com/go-playground/validator/v10
 
-go 1.13
+go 1.18
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/go-playground/assert/v2 v2.0.1
-	github.com/go-playground/locales v0.14.0
-	github.com/go-playground/universal-translator v0.18.0
-	github.com/kr/pretty v0.3.0 // indirect
-	github.com/leodido/go-urn v1.2.1
-	github.com/rogpeppe/go-internal v1.8.0 // indirect
-	github.com/stretchr/testify v1.7.0 // indirect
-	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
-	golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
-	golang.org/x/text v0.3.7
-	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	github.com/go-playground/assert/v2 v2.2.0
+	github.com/go-playground/locales v0.14.1
+	github.com/go-playground/universal-translator v0.18.1
+	github.com/leodido/go-urn v1.2.2
+	golang.org/x/crypto v0.7.0
+	golang.org/x/text v0.8.0
 )
+
+require golang.org/x/sys v0.6.0 // indirect
diff --git a/go.sum b/go.sum
index a1c43c3..cce9416 100644
--- a/go.sum
+++ b/go.sum
@@ -1,52 +1,31 @@
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
-github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
-github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
-github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
+github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/regexes.go b/regexes.go
index 9c1c634..5436e3f 100644
--- a/regexes.go
+++ b/regexes.go
@@ -64,6 +64,7 @@ const (
 	bicRegexString                   = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$`
 	semverRegexString                = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/
 	dnsRegexStringRFC1035Label       = "^[a-z]([-a-z0-9]*[a-z0-9]){0,62}$"
+	cronRegexString                  = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})`
 )
 
 var (
@@ -118,8 +119,6 @@ var (
 	btcUpperAddressRegexBech32 = regexp.MustCompile(btcAddressUpperRegexStringBech32)
 	btcLowerAddressRegexBech32 = regexp.MustCompile(btcAddressLowerRegexStringBech32)
 	ethAddressRegex            = regexp.MustCompile(ethAddressRegexString)
-	ethAddressRegexUpper       = regexp.MustCompile(ethAddressUpperRegexString)
-	ethAddressRegexLower       = regexp.MustCompile(ethAddressLowerRegexString)
 	uRLEncodedRegex            = regexp.MustCompile(uRLEncodedRegexString)
 	hTMLEncodedRegex           = regexp.MustCompile(hTMLEncodedRegexString)
 	hTMLRegex                  = regexp.MustCompile(hTMLRegexString)
@@ -128,4 +127,5 @@ var (
 	bicRegex                   = regexp.MustCompile(bicRegexString)
 	semverRegex                = regexp.MustCompile(semverRegexString)
 	dnsRegexRFC1035Label       = regexp.MustCompile(dnsRegexStringRFC1035Label)
+	cronRegex                  = regexp.MustCompile(cronRegexString)
 )
diff --git a/translations/en/en.go b/translations/en/en.go
index ee05f91..0668bb2 100644
--- a/translations/en/en.go
+++ b/translations/en/en.go
@@ -1266,6 +1266,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} must contain a valid MAC address",
 			override:    false,
 		},
+		{
+			tag:         "fqdn",
+			translation: "{0} must be a valid FQDN",
+			override:    false,
+		},
 		{
 			tag:         "unique",
 			translation: "{0} must contain unique values",
@@ -1276,6 +1281,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} must be a valid color",
 			override:    false,
 		},
+		{
+			tag:         "cron",
+			translation: "{0} must be a valid cron expression",
+			override:    false,
+		},
 		{
 			tag:         "oneof",
 			translation: "{0} must be one of [{1}]",
diff --git a/translations/es/es.go b/translations/es/es.go
index 2635408..73b06cf 100644
--- a/translations/es/es.go
+++ b/translations/es/es.go
@@ -29,6 +29,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} es un campo requerido",
 			override:    false,
 		},
+		{
+			tag:         "required_if",
+			translation: "{0} es un campo requerido",
+			override:    false,
+		},
 		{
 			tag: "len",
 			customRegisFunc: func(ut ut.Translator) (err error) {
diff --git a/translations/it/it.go b/translations/it/it.go
index 0b46fc4..8f999a4 100644
--- a/translations/it/it.go
+++ b/translations/it/it.go
@@ -124,7 +124,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 		{
 			tag: "min",
 			customRegisFunc: func(ut ut.Translator) (err error) {
-        
+
 				if err = ut.Add("min-string", "{0} deve essere lungo almeno {1}", false); err != nil {
 					return
 				}
@@ -432,7 +432,7 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 		{
 			tag: "lte",
 			customRegisFunc: func(ut ut.Translator) (err error) {
-        
+
 				if err = ut.Add("lte-string", "{0} deve essere lungo al massimo {1}", false); err != nil {
 					return
 				}
@@ -1132,6 +1132,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} deve essere un colore valido",
 			override:    false,
 		},
+		{
+			tag:         "cron",
+			translation: "{0} deve essere una stringa cron valida",
+			override:    false,
+		},
 		{
 			tag:             "oneof",
 			translation:     "{0} deve essere uno di [{1}]",


Test patch:
diff --git a/translations/en/en_test.go b/translations/en/en_test.go
index 9cb6deb..69f4ee0 100644
--- a/translations/en/en_test.go
+++ b/translations/en/en_test.go
@@ -130,6 +130,7 @@ func TestTranslations(t *testing.T) {
 		IPAddrv6          string            `validate:"ip6_addr"`
 		UinxAddr          string            `validate:"unix_addr"` // can't fail from within Go's net package currently, but maybe in the future
 		MAC               string            `validate:"mac"`
+		FQDN              string            `validate:"fqdn"`
 		IsColor           string            `validate:"iscolor"`
 		StrPtrMinLen      *string           `validate:"min=10"`
 		StrPtrMaxLen      *string           `validate:"max=1"`
@@ -226,6 +227,10 @@ func TestTranslations(t *testing.T) {
 			ns:       "Test.MAC",
 			expected: "MAC must contain a valid MAC address",
 		},
+		{
+			ns:       "Test.FQDN",
+			expected: "FQDN must be a valid FQDN",
+		},
 		{
 			ns:       "Test.IPAddr",
 			expected: "IPAddr must be a resolvable IP address",
diff --git a/translations/es/es_test.go b/translations/es/es_test.go
index 814f2ab..c2c4c81 100644
--- a/translations/es/es_test.go
+++ b/translations/es/es_test.go
@@ -35,6 +35,7 @@ func TestTranslations(t *testing.T) {
 		RequiredString    string            `validate:"required"`
 		RequiredNumber    int               `validate:"required"`
 		RequiredMultiple  []string          `validate:"required"`
+		RequiredIf        string            `validate:"required_if=Inner.RequiredIf abcd"`
 		LenString         string            `validate:"len=1"`
 		LenNumber         float64           `validate:"len=1113.00"`
 		LenMultiple       []string          `validate:"len=7"`
diff --git a/validator_test.go b/validator_test.go
index 7e314d6..fdf8a54 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -2038,10 +2038,10 @@ func TestCrossNamespaceFieldValidation(t *testing.T) {
 	Equal(t, kind, reflect.String)
 	Equal(t, current.String(), "val2")
 
-	current, _, _, ok = v.getStructFieldOKInternal(val, "Inner.CrazyNonExistantField")
+	_, _, _, ok = v.getStructFieldOKInternal(val, "Inner.CrazyNonExistantField")
 	Equal(t, ok, false)
 
-	current, _, _, ok = v.getStructFieldOKInternal(val, "Inner.Slice[101]")
+	_, _, _, ok = v.getStructFieldOKInternal(val, "Inner.Slice[101]")
 	Equal(t, ok, false)
 
 	current, kind, _, ok = v.getStructFieldOKInternal(val, "Inner.Map[key3]")
@@ -5207,6 +5207,24 @@ func TestIsNeValidation(t *testing.T) {
 	Equal(t, errs, nil)
 }
 
+func TestIsNeIgnoreCaseValidation(t *testing.T) {
+	var errs error
+	validate := New()
+	s := "abcd"
+	now := time.Now()
+
+	errs = validate.Var(s, "ne_ignore_case=efgh")
+	Equal(t, errs, nil)
+
+	errs = validate.Var(s, "ne_ignore_case=AbCd")
+	NotEqual(t, errs, nil)
+	AssertError(t, errs, "", "", "", "", "ne_ignore_case")
+
+	PanicMatches(
+		t, func() { _ = validate.Var(now, "ne_ignore_case=abcd") }, "Bad field type time.Time",
+	)
+}
+
 func TestIsEqFieldValidation(t *testing.T) {
 	var errs error
 	validate := New()
@@ -5484,6 +5502,23 @@ func TestIsEqValidation(t *testing.T) {
 	Equal(t, errs, nil)
 }
 
+func TestIsEqIgnoreCaseValidation(t *testing.T) {
+	var errs error
+	validate := New()
+	s := "abcd"
+	now := time.Now()
+
+	errs = validate.Var(s, "eq_ignore_case=abcd")
+	Equal(t, errs, nil)
+
+	errs = validate.Var(s, "eq_ignore_case=AbCd")
+	Equal(t, errs, nil)
+
+	PanicMatches(
+		t, func() { _ = validate.Var(now, "eq_ignore_case=abcd") }, "Bad field type time.Time",
+	)
+}
+
 func TestOneOfValidation(t *testing.T) {
 	validate := New()
 
@@ -5672,7 +5707,7 @@ func TestEthereumAddressValidation(t *testing.T) {
 		{"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", true},
 		{"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", true},
 		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", true},
-		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDB", false}, // Invalid checksum.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDB", true}, // Invalid checksum, but valid address.
 
 		// Other.
 		{"", false},
@@ -5703,6 +5738,56 @@ func TestEthereumAddressValidation(t *testing.T) {
 	}
 }
 
+func TestEthereumAddressChecksumValidation(t *testing.T) {
+	validate := New()
+
+	tests := []struct {
+		param    string
+		expected bool
+	}{
+		// All caps.
+		{"0x52908400098527886E0F7030069857D2E4169EE7", true},
+		{"0x8617E340B3D01FA5F11F306F4090FD50E238070D", true},
+
+		// All lower.
+		{"0x27b1fdb04752bbc536007a920d24acb045561c26", true},
+		{"0x123f681646d4a755815f9cb19e1acc8565a0c2ac", false},
+
+		// Mixed case: runs checksum validation.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", true},
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDB", false}, // Invalid checksum.
+		{"0x000000000000000000000000000000000000dead", false}, // Invalid checksum.
+		{"0x000000000000000000000000000000000000dEaD", true},  // Valid checksum.
+
+		// Other.
+		{"", false},
+		{"D1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", false},    // Missing "0x" prefix.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDbc", false}, // More than 40 hex digits.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aD", false},   // Less than 40 hex digits.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDw", false},  // Invalid hex digit "w".
+	}
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.param, "eth_addr_checksum")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d eth_addr_checksum failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d eth_addr_checksum failed Error: %s", i, errs)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "eth_addr_checksum" {
+					t.Fatalf("Index: %d Latitude failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+}
+
 func TestBitcoinAddressValidation(t *testing.T) {
 	validate := New()
 
@@ -7793,6 +7878,77 @@ func TestUrl(t *testing.T) {
 	PanicMatches(t, func() { _ = validate.Var(i, "url") }, "Bad field type int")
 }
 
+func TestHttpUrl(t *testing.T) {
+	tests := []struct {
+		param    string
+		expected bool
+	}{
+		{"http://foo.bar#com", true},
+		{"http://foobar.com", true},
+		{"HTTP://foobar.com", true},
+		{"https://foobar.com", true},
+		{"foobar.com", false},
+		{"http://foobar.coffee/", true},
+		{"http://foobar.中文网/", true},
+		{"http://foobar.org/", true},
+		{"http://foobar.org:8080/", true},
+		{"ftp://foobar.ru/", false},
+		{"file:///etc/passwd", false},
+		{"file://C:/windows/win.ini", false},
+		{"http://user:pass@www.foobar.com/", true},
+		{"http://127.0.0.1/", true},
+		{"http://duckduckgo.com/?q=%2F", true},
+		{"http://localhost:3000/", true},
+		{"http://foobar.com/?foo=bar#baz=qux", true},
+		{"http://foobar.com?foo=bar", true},
+		{"http://www.xn--froschgrn-x9a.net/", true},
+		{"", false},
+		{"a://b", false},
+		{"xyz://foobar.com", false},
+		{"invalid.", false},
+		{".com", false},
+		{"rtmp://foobar.com", false},
+		{"http://www.foo_bar.com/", true},
+		{"http://localhost:3000/", true},
+		{"http://foobar.com/#baz", true},
+		{"http://foobar.com#baz=qux", true},
+		{"http://foobar.com/t$-_.+!*\\'(),", true},
+		{"http://www.foobar.com/~foobar", true},
+		{"http://www.-foobar.com/", true},
+		{"http://www.foo---bar.com/", true},
+		{"mailto:someone@example.com", false},
+		{"irc://irc.server.org/channel", false},
+		{"irc://#channel@network", false},
+		{"/abs/test/dir", false},
+		{"./rel/test/dir", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.param, "http_url")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d HTTP URL failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d HTTP URL failed Error: %s", i, errs)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "http_url" {
+					t.Fatalf("Index: %d HTTP URL failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+
+	i := 1
+	PanicMatches(t, func() { _ = validate.Var(i, "http_url") }, "Bad field type int")
+}
+
 func TestUri(t *testing.T) {
 	tests := []struct {
 		param    string
@@ -9805,6 +9961,12 @@ func TestUniqueValidation(t *testing.T) {
 		{map[string]string{"one": "a", "two": "a"}, false},
 		{map[string]interface{}{"one": "a", "two": "a"}, false},
 		{map[string]interface{}{"one": "a", "two": 1, "three": "b", "four": 1}, false},
+		{map[string]*string{"one": stringPtr("a"), "two": stringPtr("a")}, false},
+		{map[string]*string{"one": stringPtr("a"), "two": stringPtr("b")}, true},
+		{map[string]*int{"one": intPtr(1), "two": intPtr(1)}, false},
+		{map[string]*int{"one": intPtr(1), "two": intPtr(2)}, true},
+		{map[string]*float64{"one": float64Ptr(1.1), "two": float64Ptr(1.1)}, false},
+		{map[string]*float64{"one": float64Ptr(1.1), "two": float64Ptr(1.2)}, true},
 	}
 
 	validate := New()
@@ -9829,6 +9991,41 @@ func TestUniqueValidation(t *testing.T) {
 		}
 	}
 	PanicMatches(t, func() { _ = validate.Var(1.0, "unique") }, "Bad field type float64")
+
+	t.Run("struct", func(t *testing.T) {
+		tests := []struct {
+			param    interface{}
+			expected bool
+		}{
+			{struct {
+				A string `validate:"unique=B"`
+				B string
+			}{A: "abc", B: "bcd"}, true},
+			{struct {
+				A string `validate:"unique=B"`
+				B string
+			}{A: "abc", B: "abc"}, false},
+		}
+		validate := New()
+
+		for i, test := range tests {
+			errs := validate.Struct(test.param)
+			if test.expected {
+				if !IsEqual(errs, nil) {
+					t.Fatalf("Index: %d unique failed Error: %v", i, errs)
+				}
+			} else {
+				if IsEqual(errs, nil) {
+					t.Fatalf("Index: %d unique failed Error: %v", i, errs)
+				} else {
+					val := getError(errs, "A", "A")
+					if val.Tag() != "unique" {
+						t.Fatalf("Index: %d unique failed Error: %v", i, errs)
+					}
+				}
+			}
+		}
+	})
 }
 
 func TestUniqueValidationStructSlice(t *testing.T) {
@@ -9881,6 +10078,7 @@ func TestUniqueValidationStructPtrSlice(t *testing.T) {
 	}{
 		{A: stringPtr("one"), B: stringPtr("two")},
 		{A: stringPtr("one"), B: stringPtr("three")},
+		{},
 	}
 
 	tests := []struct {
@@ -11174,7 +11372,7 @@ func TestExcludedUnless(t *testing.T) {
 		FieldE  string `validate:"omitempty" json:"field_e"`
 		FieldER string `validate:"excluded_unless=FieldE test" json:"field_er"`
 	}{
-		FieldE:  "notest",
+		FieldE:  "test",
 		FieldER: "filled",
 	}
 	errs := validate.Struct(test)
@@ -11184,7 +11382,7 @@ func TestExcludedUnless(t *testing.T) {
 		FieldE  string `validate:"omitempty" json:"field_e"`
 		FieldER string `validate:"excluded_unless=FieldE test" json:"field_er"`
 	}{
-		FieldE:  "test",
+		FieldE:  "notest",
 		FieldER: "filled",
 	}
 	errs = validate.Struct(test2)
@@ -11193,7 +11391,26 @@ func TestExcludedUnless(t *testing.T) {
 	Equal(t, len(ve), 1)
 	AssertError(t, errs, "FieldER", "FieldER", "FieldER", "FieldER", "excluded_unless")
 
-	shouldError := "test"
+	// test5 and test6: excluded_unless has no effect if FieldER is left blank
+	test5 := struct {
+		FieldE  string `validate:"omitempty" json:"field_e"`
+		FieldER string `validate:"excluded_unless=FieldE test" json:"field_er"`
+	}{
+		FieldE: "test",
+	}
+	errs = validate.Struct(test5)
+	Equal(t, errs, nil)
+
+	test6 := struct {
+		FieldE  string `validate:"omitempty" json:"field_e"`
+		FieldER string `validate:"excluded_unless=FieldE test" json:"field_er"`
+	}{
+		FieldE: "notest",
+	}
+	errs = validate.Struct(test6)
+	Equal(t, errs, nil)
+
+	shouldError := "notest"
 	test3 := struct {
 		Inner  *Inner
 		Field1 string `validate:"excluded_unless=Inner.Field test" json:"field_1"`
@@ -11207,7 +11424,7 @@ func TestExcludedUnless(t *testing.T) {
 	Equal(t, len(ve), 1)
 	AssertError(t, errs, "Field1", "Field1", "Field1", "Field1", "excluded_unless")
 
-	shouldPass := "shouldPass"
+	shouldPass := "test"
 	test4 := struct {
 		Inner  *Inner
 		FieldE string `validate:"omitempty" json:"field_e"`
@@ -11219,6 +11436,26 @@ func TestExcludedUnless(t *testing.T) {
 	errs = validate.Struct(test4)
 	Equal(t, errs, nil)
 
+	// test7 and test8: excluded_unless has no effect if FieldER is left blank
+	test7 := struct {
+		Inner   *Inner
+		FieldE  string `validate:"omitempty" json:"field_e"`
+		FieldER string `validate:"excluded_unless=Inner.Field test" json:"field_er"`
+	}{
+		FieldE: "test",
+	}
+	errs = validate.Struct(test7)
+	Equal(t, errs, nil)
+
+	test8 := struct {
+		FieldE  string `validate:"omitempty" json:"field_e"`
+		FieldER string `validate:"excluded_unless=Inner.Field test" json:"field_er"`
+	}{
+		FieldE: "test",
+	}
+	errs = validate.Struct(test8)
+	Equal(t, errs, nil)
+
 	// Checks number of params in struct tag is correct
 	defer func() {
 		if r := recover(); r == nil {
@@ -12323,3 +12560,37 @@ func TestMultiOrOperatorGroup(t *testing.T) {
 		}
 	}
 }
+
+func TestCronExpressionValidation(t *testing.T) {
+	tests := []struct {
+		value    string `validate:"cron"`
+		tag      string
+		expected bool
+	}{
+		{"0 0 12 * * ?", "cron", true},
+		{"0 15 10 ? * *", "cron", true},
+		{"0 15 10 * * ?", "cron", true},
+		{"0 15 10 * * ? 2005", "cron", true},
+		{"0 15 10 ? * 6L", "cron", true},
+		{"0 15 10 ? * 6L 2002-2005", "cron", true},
+		{"*/20 * * * *", "cron", true},
+		{"0 15 10 ? * MON-FRI", "cron", true},
+		{"0 15 10 ? * 6#3", "cron", true},
+		{"wrong", "cron", 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 cron "%s" failed Error: %s`, i, test.value, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf(`Index: %d cron "%s" should have errs`, i, test.value)
+			}
+		}
+	}
+}