Repository: go-playground/validator
Pull request #1187 (2023-10-28 10:25:38)

Add `omitnil` modifier
## Fixes Or Enhances

Related issue: https://github.com/go-playground/validator/issues/1186

**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/63602366/badge)](https://coveralls.io/builds/63602366)

coverage: 73.849%. remained the same when pulling **8a856d61150dc24c92d195a8b0f7a1008b122877 on tarampampam:master** into **94a637ab9fbbb0bc0fe8a278f0352d0b14e2c365 on go-playground:master**.

tarampampam: Friendly ping @yjagdale @deankarn 😃 

Patch:
diff --git a/_examples/simple/main.go b/_examples/simple/main.go
index 35a56c1..6581412 100644
--- a/_examples/simple/main.go
+++ b/_examples/simple/main.go
@@ -12,7 +12,7 @@ type User struct {
 	LastName       string     `validate:"required"`
 	Age            uint8      `validate:"gte=0,lte=130"`
 	Email          string     `validate:"required,email"`
-	Gender         string     `validate:"oneof=male female prefer_not_to`
+	Gender         string     `validate:"oneof=male female prefer_not_to"`
 	FavouriteColor string     `validate:"iscolor"`                // alias for 'hexcolor|rgb|rgba|hsl|hsla'
 	Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
 }
diff --git a/baked_in.go b/baked_in.go
index 0b62330..ac1c350 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -51,6 +51,7 @@ var (
 		endKeysTag:        {},
 		structOnlyTag:     {},
 		omitempty:         {},
+		omitnil:           {},
 		skipValidationTag: {},
 		utf8HexComma:      {},
 		utf8Pipe:          {},
@@ -508,47 +509,47 @@ func isASCII(fl FieldLevel) bool {
 
 // isUUID5 is the validation function for validating if the field's value is a valid v5 UUID.
 func isUUID5(fl FieldLevel) bool {
-	return uUID5Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUID5Regex, fl)
 }
 
 // isUUID4 is the validation function for validating if the field's value is a valid v4 UUID.
 func isUUID4(fl FieldLevel) bool {
-	return uUID4Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUID4Regex, fl)
 }
 
 // isUUID3 is the validation function for validating if the field's value is a valid v3 UUID.
 func isUUID3(fl FieldLevel) bool {
-	return uUID3Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUID3Regex, fl)
 }
 
 // isUUID is the validation function for validating if the field's value is a valid UUID of any version.
 func isUUID(fl FieldLevel) bool {
-	return uUIDRegex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUIDRegex, fl)
 }
 
 // isUUID5RFC4122 is the validation function for validating if the field's value is a valid RFC4122 v5 UUID.
 func isUUID5RFC4122(fl FieldLevel) bool {
-	return uUID5RFC4122Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUID5RFC4122Regex, fl)
 }
 
 // isUUID4RFC4122 is the validation function for validating if the field's value is a valid RFC4122 v4 UUID.
 func isUUID4RFC4122(fl FieldLevel) bool {
-	return uUID4RFC4122Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUID4RFC4122Regex, fl)
 }
 
 // isUUID3RFC4122 is the validation function for validating if the field's value is a valid RFC4122 v3 UUID.
 func isUUID3RFC4122(fl FieldLevel) bool {
-	return uUID3RFC4122Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUID3RFC4122Regex, fl)
 }
 
 // isUUIDRFC4122 is the validation function for validating if the field's value is a valid RFC4122 UUID of any version.
 func isUUIDRFC4122(fl FieldLevel) bool {
-	return uUIDRFC4122Regex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uUIDRFC4122Regex, fl)
 }
 
 // isULID is the validation function for validating if the field's value is a valid ULID.
 func isULID(fl FieldLevel) bool {
-	return uLIDRegex.MatchString(fl.Field().String())
+	return fieldMatchesRegexByStringerValOrString(uLIDRegex, fl)
 }
 
 // isMD4 is the validation function for validating if the field's value is a valid MD4.
diff --git a/cache.go b/cache.go
index bbfd2a4..0f4fa6b 100644
--- a/cache.go
+++ b/cache.go
@@ -20,6 +20,7 @@ const (
 	typeOr
 	typeKeys
 	typeEndKeys
+	typeOmitNil
 )
 
 const (
@@ -252,6 +253,10 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
 			current.typeof = typeOmitEmpty
 			continue
 
+		case omitnil:
+			current.typeof = typeOmitNil
+			continue
+
 		case structOnlyTag:
 			current.typeof = typeStructOnly
 			continue
diff --git a/doc.go b/doc.go
index c4dbb59..b474091 100644
--- a/doc.go
+++ b/doc.go
@@ -194,6 +194,13 @@ such as min or max won't run, but if a value is set validation will run.
 
 	Usage: omitempty
 
+# Omit Nil
+
+Allows to skip the validation if the value is nil (same as omitempty, but
+only for the nil-values).
+
+	Usage: omitnil
+
 # Dive
 
 This tells the validator to dive into a slice, array or map and validate that
diff --git a/util.go b/util.go
index 4bd947b..1685159 100644
--- a/util.go
+++ b/util.go
@@ -1,7 +1,9 @@
 package validator
 
 import (
+	"fmt"
 	"reflect"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -292,3 +294,18 @@ func panicIf(err error) {
 		panic(err.Error())
 	}
 }
+
+// Checks if field value matches regex. If fl.Field can be cast to Stringer, it uses the Stringer interfaces
+// String() return value. Otherwise, it uses fl.Field's String() value.
+func fieldMatchesRegexByStringerValOrString(regex *regexp.Regexp, fl FieldLevel) bool {
+	switch fl.Field().Kind() {
+	case reflect.String:
+		return regex.MatchString(fl.Field().String())
+	default:
+		if stringer, ok := fl.Field().Interface().(fmt.Stringer); ok {
+			return regex.MatchString(stringer.String())
+		} else {
+			return regex.MatchString(fl.Field().String())
+		}
+	}
+}
diff --git a/validator.go b/validator.go
index 342c4ec..a072d39 100644
--- a/validator.go
+++ b/validator.go
@@ -112,6 +112,10 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
 			return
 		}
 
+		if ct.typeof == typeOmitNil && (kind != reflect.Invalid && current.IsNil()) {
+			return
+		}
+
 		if ct.hasTag {
 			if kind == reflect.Invalid {
 				v.str1 = string(append(ns, cf.altName...))
@@ -233,6 +237,26 @@ OUTER:
 			ct = ct.next
 			continue
 
+		case typeOmitNil:
+			v.slflParent = parent
+			v.flField = current
+			v.cf = cf
+			v.ct = ct
+
+			switch field := v.Field(); field.Kind() {
+			case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
+				if field.IsNil() {
+					return
+				}
+			default:
+				if v.fldIsPointer && field.Interface() == nil {
+					return
+				}
+			}
+
+			ct = ct.next
+			continue
+
 		case typeEndKeys:
 			return
 
diff --git a/validator_instance.go b/validator_instance.go
index a4dbdd0..d5a7be1 100644
--- a/validator_instance.go
+++ b/validator_instance.go
@@ -22,6 +22,7 @@ const (
 	structOnlyTag         = "structonly"
 	noStructLevelTag      = "nostructlevel"
 	omitempty             = "omitempty"
+	omitnil               = "omitnil"
 	isdefault             = "isdefault"
 	requiredWithoutAllTag = "required_without_all"
 	requiredWithoutTag    = "required_without"


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 230ab61..e246611 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -4105,6 +4105,23 @@ func TestUUID3Validation(t *testing.T) {
 	}
 }
 
+type uuidTestType struct {
+	val string
+}
+
+func (u uuidTestType) String() string {
+	return u.val
+}
+
+type uuidAlias string
+
+func (u uuidAlias) String() string {
+	return "This is a UUID " + string(u)
+}
+
+var _ fmt.Stringer = uuidTestType{}
+var _ fmt.Stringer = uuidAlias("")
+
 func TestUUIDValidation(t *testing.T) {
 	tests := []struct {
 		param    string
@@ -4141,6 +4158,31 @@ func TestUUIDValidation(t *testing.T) {
 			}
 		}
 	}
+
+	// Test UUID validation on uuid structs type that implements Stringer interface.
+	structWithValidUUID := struct {
+		UUID uuidTestType `validate:"uuid"`
+	}{
+		UUID: uuidTestType{val: "a987fbc9-4bed-3078-cf07-9141ba07c9f3"},
+	}
+	structWithInvalidUUID := struct {
+		UUID uuidTestType `validate:"uuid"`
+	}{
+		UUID: uuidTestType{val: "934859"},
+	}
+
+	if err := validate.Struct(structWithValidUUID); err != nil {
+		t.Fatalf("UUID failed Error: %s", err)
+	}
+	if err := validate.Struct(structWithInvalidUUID); err == nil {
+		t.Fatal("UUID failed Error expected but received nil")
+	}
+
+	// Test on Alias type with Stringer interface.
+	alias := uuidAlias("a987fbc9-4bed-3078-cf07-9141ba07c9f3")
+	if err := validate.Var(alias, "uuid"); err != nil {
+		t.Fatalf("UUID failed Error: %s", err)
+	}
 }
 
 func TestUUID5RFC4122Validation(t *testing.T) {
@@ -13154,14 +13196,14 @@ func TestSpiceDBValueFormatValidation(t *testing.T) {
 		tag      string
 		expected bool
 	}{
-		//Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
+		// Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
 		{"*", "spicedb=id", true},
 		{`azAZ09_|/-=+`, "spicedb=id", true},
 		{`a*`, "spicedb=id", false},
 		{`/`, "spicedb=id", true},
 		{"*", "spicedb", true},
 
-		//Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
+		// Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
 		{"a", "spicedb=permission", false},
 		{"1", "spicedb=permission", false},
 		{"a1", "spicedb=permission", false},
@@ -13171,7 +13213,7 @@ func TestSpiceDBValueFormatValidation(t *testing.T) {
 		{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", true},
 		{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", false},
 
-		//Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
+		// Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
 		{"a", "spicedb=type", false},
 		{"1", "spicedb=type", false},
 		{"a1", "spicedb=type", false},
@@ -13564,3 +13606,47 @@ func TestTimeRequired(t *testing.T) {
 	NotEqual(t, err, nil)
 	AssertError(t, err.(ValidationErrors), "TestTime.Time", "TestTime.Time", "Time", "Time", "required")
 }
+
+func TestOmitNilAndRequired(t *testing.T) {
+	type (
+		OmitEmpty struct {
+			Str    string  `validate:"omitempty,required,min=10"`
+			StrPtr *string `validate:"omitempty,required,min=10"`
+			Inner  *OmitEmpty
+		}
+		OmitNil struct {
+			Str    string  `validate:"omitnil,required,min=10"`
+			StrPtr *string `validate:"omitnil,required,min=10"`
+			Inner  *OmitNil
+		}
+	)
+
+	var (
+		validate = New(WithRequiredStructEnabled())
+		valid    = "this is the long string to pass the validation rule"
+	)
+
+	t.Run("compare using valid data", func(t *testing.T) {
+		err1 := validate.Struct(OmitEmpty{Str: valid, StrPtr: &valid, Inner: &OmitEmpty{Str: valid, StrPtr: &valid}})
+		err2 := validate.Struct(OmitNil{Str: valid, StrPtr: &valid, Inner: &OmitNil{Str: valid, StrPtr: &valid}})
+
+		Equal(t, err1, nil)
+		Equal(t, err2, nil)
+	})
+
+	t.Run("compare fully empty omitempty and omitnil", func(t *testing.T) {
+		err1 := validate.Struct(OmitEmpty{})
+		err2 := validate.Struct(OmitNil{})
+
+		Equal(t, err1, nil)
+		AssertError(t, err2, "OmitNil.Str", "OmitNil.Str", "Str", "Str", "required")
+	})
+
+	t.Run("validate in deep", func(t *testing.T) {
+		err1 := validate.Struct(OmitEmpty{Str: valid, Inner: &OmitEmpty{}})
+		err2 := validate.Struct(OmitNil{Str: valid, Inner: &OmitNil{}})
+
+		Equal(t, err1, nil)
+		AssertError(t, err2, "OmitNil.Inner.Str", "OmitNil.Inner.Str", "Str", "Str", "required")
+	})
+}