Repository: go-playground/validator
Pull request #1394 (2025-03-07 02:51:53)

Add translation example

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

coverage: 74.466%. remained the same
when pulling **59d14ead345efa9252725a082903b6f466d14902 on cxlblm:translation**
into **bae7f6d3cb253fbd5dddcdf339f8460fa8d091c3 on go-playground:master**.


Patch:
diff --git a/README.md b/README.md
index df0649f..28f7e15 100644
--- a/README.md
+++ b/README.md
@@ -262,6 +262,8 @@ validate := validator.New(validator.WithRequiredStructEnabled())
 | excluded_without | Excluded Without |
 | excluded_without_all | Excluded Without All |
 | unique | Unique |
+| validateFn | Verify if the method `Validate() error` does not return an error (or any specified method) |
+
 
 #### Aliases:
 | Tag | Description |
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/validate_fn/enum_enumer.go b/_examples/validate_fn/enum_enumer.go
new file mode 100644
index 0000000..15e16b5
--- /dev/null
+++ b/_examples/validate_fn/enum_enumer.go
@@ -0,0 +1,86 @@
+// Code generated by "enumer -type=Enum"; DO NOT EDIT.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+const _EnumName = "ZeroOneTwoThree"
+
+var _EnumIndex = [...]uint8{0, 4, 7, 10, 15}
+
+const _EnumLowerName = "zeroonetwothree"
+
+func (i Enum) String() string {
+	if i >= Enum(len(_EnumIndex)-1) {
+		return fmt.Sprintf("Enum(%d)", i)
+	}
+	return _EnumName[_EnumIndex[i]:_EnumIndex[i+1]]
+}
+
+// An "invalid array index" compiler error signifies that the constant values have changed.
+// Re-run the stringer command to generate them again.
+func _EnumNoOp() {
+	var x [1]struct{}
+	_ = x[Zero-(0)]
+	_ = x[One-(1)]
+	_ = x[Two-(2)]
+	_ = x[Three-(3)]
+}
+
+var _EnumValues = []Enum{Zero, One, Two, Three}
+
+var _EnumNameToValueMap = map[string]Enum{
+	_EnumName[0:4]:        Zero,
+	_EnumLowerName[0:4]:   Zero,
+	_EnumName[4:7]:        One,
+	_EnumLowerName[4:7]:   One,
+	_EnumName[7:10]:       Two,
+	_EnumLowerName[7:10]:  Two,
+	_EnumName[10:15]:      Three,
+	_EnumLowerName[10:15]: Three,
+}
+
+var _EnumNames = []string{
+	_EnumName[0:4],
+	_EnumName[4:7],
+	_EnumName[7:10],
+	_EnumName[10:15],
+}
+
+// EnumString retrieves an enum value from the enum constants string name.
+// Throws an error if the param is not part of the enum.
+func EnumString(s string) (Enum, error) {
+	if val, ok := _EnumNameToValueMap[s]; ok {
+		return val, nil
+	}
+
+	if val, ok := _EnumNameToValueMap[strings.ToLower(s)]; ok {
+		return val, nil
+	}
+	return 0, fmt.Errorf("%s does not belong to Enum values", s)
+}
+
+// EnumValues returns all values of the enum
+func EnumValues() []Enum {
+	return _EnumValues
+}
+
+// EnumStrings returns a slice of all String values of the enum
+func EnumStrings() []string {
+	strs := make([]string, len(_EnumNames))
+	copy(strs, _EnumNames)
+	return strs
+}
+
+// IsAEnum returns "true" if the value is listed in the enum definition. "false" otherwise
+func (i Enum) IsAEnum() bool {
+	for _, v := range _EnumValues {
+		if i == v {
+			return true
+		}
+	}
+	return false
+}
diff --git a/_examples/validate_fn/go.mod b/_examples/validate_fn/go.mod
new file mode 100644
index 0000000..4f7124b
--- /dev/null
+++ b/_examples/validate_fn/go.mod
@@ -0,0 +1,18 @@
+module github.com/peczenyj/validator/_examples/validate_fn
+
+go 1.20
+
+replace github.com/go-playground/validator/v10 => ../../../validator
+
+require github.com/go-playground/validator/v10 v10.26.0
+
+require (
+	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+	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.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
new file mode 100644
index 0000000..acb0dc1
--- /dev/null
+++ b/_examples/validate_fn/go.sum
@@ -0,0 +1,31 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+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/_examples/validate_fn/main.go b/_examples/validate_fn/main.go
new file mode 100644
index 0000000..cf61a29
--- /dev/null
+++ b/_examples/validate_fn/main.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/go-playground/validator/v10"
+)
+
+//go:generate enumer -type=Enum
+type Enum uint8
+
+const (
+	Zero Enum = iota
+	One
+	Two
+	Three
+)
+
+func (e *Enum) Validate() error {
+	if e == nil {
+		return errors.New("can't be nil")
+	}
+
+	return nil
+}
+
+type Struct struct {
+	Foo *Enum `validate:"validateFn"`         // uses Validate() error by default
+	Bar Enum  `validate:"validateFn=IsAEnum"` // uses IsAEnum() bool provided by enumer
+}
+
+func main() {
+	validate := validator.New()
+
+	var x Struct
+
+	x.Bar = Enum(64)
+
+	if err := validate.Struct(x); err != nil {
+		fmt.Printf("Expected Err(s):\n%+v\n", err)
+	}
+
+	x = Struct{
+		Foo: new(Enum),
+		Bar: One,
+	}
+
+	if err := validate.Struct(x); err != nil {
+		fmt.Printf("Unexpected Err(s):\n%+v\n", err)
+	}
+}
diff --git a/baked_in.go b/baked_in.go
index fe36121..f7e8948 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -2,10 +2,12 @@ package validator
 
 import (
 	"bytes"
+	"cmp"
 	"context"
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/fs"
 	"net"
@@ -244,6 +246,7 @@ var (
 		"cron":                          isCron,
 		"spicedb":                       isSpiceDB,
 		"ein":                           isEIN,
+		"validateFn":                    isValidateFn,
 	}
 )
 
@@ -2001,8 +2004,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
 }
@@ -3046,3 +3052,60 @@ func isEIN(fl FieldLevel) bool {
 
 	return einRegex().MatchString(field.String())
 }
+
+func isValidateFn(fl FieldLevel) bool {
+	const defaultParam = `Validate`
+
+	field := fl.Field()
+	validateFn := cmp.Or(fl.Param(), defaultParam)
+
+	ok, err := tryCallValidateFn(field, validateFn)
+	if err != nil {
+		return false
+	}
+
+	return ok
+}
+
+var (
+	errMethodNotFound          = errors.New(`method not found`)
+	errMethodReturnNoValues    = errors.New(`method return o values (void)`)
+	errMethodReturnInvalidType = errors.New(`method should return invalid type`)
+)
+
+func tryCallValidateFn(field reflect.Value, validateFn string) (bool, error) {
+	method := field.MethodByName(validateFn)
+	if field.CanAddr() && !method.IsValid() {
+		method = field.Addr().MethodByName(validateFn)
+	}
+
+	if !method.IsValid() {
+		return false, fmt.Errorf("unable to call %q on type %q: %w",
+			validateFn, field.Type().String(), errMethodNotFound)
+	}
+
+	returnValues := method.Call([]reflect.Value{})
+	if len(returnValues) == 0 {
+		return false, fmt.Errorf("unable to use result of method %q on type %q: %w",
+			validateFn, field.Type().String(), errMethodReturnNoValues)
+	}
+
+	firstReturnValue := returnValues[0]
+
+	switch firstReturnValue.Kind() {
+	case reflect.Bool:
+		return firstReturnValue.Bool(), nil
+	case reflect.Interface:
+		errorType := reflect.TypeOf((*error)(nil)).Elem()
+
+		if firstReturnValue.Type().Implements(errorType) {
+			return firstReturnValue.IsNil(), nil
+		}
+
+		return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got interface %v expect error)",
+			validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
+	default:
+		return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got %v expect error or bool)",
+			validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
+	}
+}
diff --git a/doc.go b/doc.go
index e7a241f..91bace7 100644
--- a/doc.go
+++ b/doc.go
@@ -756,6 +756,20 @@ in a field of the struct specified via a parameter.
 	// For slices of struct:
 	Usage: unique=field
 
+# ValidateFn
+
+This validates that an object responds to a method that can return error or bool.
+By default it expects an interface `Validate() error` and check that the method
+does not return an error. Other methods can be specified using two signatures:
+If the method returns an error, it check if the return value is nil.
+If the method returns a boolean, it checks if the value is true.
+
+	// to use the default method Validate() error
+	Usage: validateFn
+
+	// to use the custom method IsValid() bool (or error)
+	Usage: validateFn=IsValid
+
 # Alpha Only
 
 This validates that a string value contains ASCII alpha characters only
diff --git a/translations/en/en.go b/translations/en/en.go
index d0161c7..9458edc 100644
--- a/translations/en/en.go
+++ b/translations/en/en.go
@@ -1484,6 +1484,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} must be a valid cve identifier",
 			override:    false,
 		},
+		{
+			tag:         "validateFn",
+			translation: "{0} must be a valid object",
+			override:    false,
+		},
 	}
 
 	for _, t := range translations {
diff --git a/translations/pt_BR/pt_BR.go b/translations/pt_BR/pt_BR.go
index 0e43848..27cb2a5 100644
--- a/translations/pt_BR/pt_BR.go
+++ b/translations/pt_BR/pt_BR.go
@@ -1292,6 +1292,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
 			translation: "{0} deve ser um identificador cve válido",
 			override:    false,
 		},
+		{
+			tag:         "validateFn",
+			translation: "{0} deve ser um objeto válido",
+			override:    false,
+		},
 	}
 
 	for _, t := range translations {
diff --git a/validator_instance.go b/validator_instance.go
index e68a870..9362cd7 100644
--- a/validator_instance.go
+++ b/validator_instance.go
@@ -231,23 +231,6 @@ func (v *Validate) RegisterValidationCtx(tag string, fn FuncCtx, callValidationE
 	return v.registerValidation(tag, fn, false, nilCheckable)
 }
 
-func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool, nilCheckable bool) error {
-	if len(tag) == 0 {
-		return errors.New("function Key cannot be empty")
-	}
-
-	if fn == nil {
-		return errors.New("function cannot be empty")
-	}
-
-	_, ok := restrictedTags[tag]
-	if !bakedIn && (ok || strings.ContainsAny(tag, restrictedTagChars)) {
-		panic(fmt.Sprintf(restrictedTagErr, tag))
-	}
-	v.validations[tag] = internalValidationFuncWrapper{fn: fn, runValidationOnNil: nilCheckable}
-	return nil
-}
-
 // RegisterAlias registers a mapping of a single validation tag that
 // defines a common or complex set of validation(s) to simplify adding validation
 // to structs.
@@ -697,3 +680,20 @@ func (v *Validate) VarWithValueCtx(ctx context.Context, field interface{}, other
 	v.pool.Put(vd)
 	return
 }
+
+func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool, nilCheckable bool) error {
+	if len(tag) == 0 {
+		return errors.New("function Key cannot be empty")
+	}
+
+	if fn == nil {
+		return errors.New("function cannot be empty")
+	}
+
+	_, ok := restrictedTags[tag]
+	if !bakedIn && (ok || strings.ContainsAny(tag, restrictedTagChars)) {
+		panic(fmt.Sprintf(restrictedTagErr, tag))
+	}
+	v.validations[tag] = internalValidationFuncWrapper{fn: fn, runValidationOnNil: nilCheckable}
+	return nil
+}


Test patch:
diff --git a/benchmarks_test.go b/benchmarks_test.go
index ee70f95..14de8e6 100644
--- a/benchmarks_test.go
+++ b/benchmarks_test.go
@@ -3,6 +3,7 @@ package validator
 import (
 	"bytes"
 	sql "database/sql/driver"
+	"errors"
 	"testing"
 	"time"
 )
@@ -1097,3 +1098,39 @@ func BenchmarkOneofParallel(b *testing.B) {
 		}
 	})
 }
+
+type T struct{}
+
+func (*T) Validate() error { return errors.New("ops") }
+
+func BenchmarkValidateFnSequencial(b *testing.B) {
+	validate := New()
+
+	type Test struct {
+		T T `validate:"validateFn"`
+	}
+
+	test := &Test{}
+
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		_ = validate.Struct(test)
+	}
+}
+
+func BenchmarkValidateFnParallel(b *testing.B) {
+	validate := New()
+
+	type Test struct {
+		T T `validate:"validateFn"`
+	}
+
+	test := &Test{}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			_ = validate.Struct(test)
+		}
+	})
+}
diff --git a/translations/en/en_test.go b/translations/en/en_test.go
index 7ae4d00..44ce09a 100644
--- a/translations/en/en_test.go
+++ b/translations/en/en_test.go
@@ -10,6 +10,10 @@ import (
 	"github.com/go-playground/validator/v10"
 )
 
+type Foo struct{}
+
+func (Foo) IsBar() bool { return false }
+
 func TestTranslations(t *testing.T) {
 	eng := english.New()
 	uni := ut.New(eng, eng)
@@ -181,6 +185,7 @@ func TestTranslations(t *testing.T) {
 		CveString          string        `validate:"cve"`
 		MinDuration        time.Duration `validate:"min=1h30m,max=2h"`
 		MaxDuration        time.Duration `validate:"min=1h30m,max=2h"`
+		ValidateFn         Foo           `validate:"validateFn=IsBar"`
 	}
 
 	var test Test
@@ -805,6 +810,10 @@ func TestTranslations(t *testing.T) {
 			ns:       "Test.MaxDuration",
 			expected: "MaxDuration must be 2h or less",
 		},
+		{
+			ns:       "Test.ValidateFn",
+			expected: "ValidateFn must be a valid object",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/translations/pt_BR/pt_BR_test.go b/translations/pt_BR/pt_BR_test.go
index aee97d8..e5880d9 100644
--- a/translations/pt_BR/pt_BR_test.go
+++ b/translations/pt_BR/pt_BR_test.go
@@ -10,6 +10,10 @@ import (
 	"github.com/go-playground/validator/v10"
 )
 
+type Foo struct{}
+
+func (Foo) IsBar() bool { return false }
+
 func TestTranslations(t *testing.T) {
 	ptbr := brazilian_portuguese.New()
 	uni := ut.New(ptbr, ptbr)
@@ -142,6 +146,7 @@ func TestTranslations(t *testing.T) {
 		BooleanString     string    `validate:"boolean"`
 		Image             string    `validate:"image"`
 		CveString         string    `validate:"cve"`
+		ValidateFn        Foo       `validate:"validateFn=IsBar"`
 	}
 
 	var test Test
@@ -640,6 +645,10 @@ func TestTranslations(t *testing.T) {
 			ns:       "Test.CveString",
 			expected: "CveString deve ser um identificador cve válido",
 		},
+		{
+			ns:       "Test.ValidateFn",
+			expected: "ValidateFn deve ser um objeto válido",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/validator_test.go b/validator_test.go
index 51bc44c..85284ec 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -7,6 +7,7 @@ import (
 	"database/sql/driver"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"image"
 	"image/jpeg"
@@ -11864,6 +11865,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) {
@@ -14148,3 +14175,147 @@ func TestPrivateFieldsStruct(t *testing.T) {
 		Equal(t, len(errs), tc.errorNum)
 	}
 }
+
+type NotRed struct {
+	Color string
+}
+
+func (r *NotRed) Validate() error {
+	if r != nil && r.Color == "red" {
+		return errors.New("should not be red")
+	}
+
+	return nil
+}
+
+func (r NotRed) IsNotRed() bool {
+	return r.Color != "red"
+}
+
+func (r NotRed) DoNothing() {}
+
+func (r NotRed) String() string { return "not red instance" }
+
+func TestValidateFn(t *testing.T) {
+	t.Run("using pointer", func(t *testing.T) {
+		validate := New()
+
+		type Test struct {
+			String string
+			Inner  *NotRed `validate:"validateFn"`
+		}
+
+		var tt Test
+
+		errs := validate.Struct(tt)
+		NotEqual(t, errs, nil)
+
+		fe := errs.(ValidationErrors)[0]
+		Equal(t, fe.Field(), "Inner")
+		Equal(t, fe.Namespace(), "Test.Inner")
+		Equal(t, fe.Tag(), "validateFn")
+
+		tt.Inner = &NotRed{Color: "blue"}
+		errs = validate.Struct(tt)
+		Equal(t, errs, nil)
+
+		tt.Inner = &NotRed{Color: "red"}
+		errs = validate.Struct(tt)
+		NotEqual(t, errs, nil)
+
+		fe = errs.(ValidationErrors)[0]
+		Equal(t, fe.Field(), "Inner")
+		Equal(t, fe.Namespace(), "Test.Inner")
+		Equal(t, fe.Tag(), "validateFn")
+	})
+
+	t.Run("using struct", func(t *testing.T) {
+		validate := New()
+
+		type Test2 struct {
+			String string
+			Inner  NotRed `validate:"validateFn"`
+		}
+
+		var tt2 Test2
+
+		errs := validate.Struct(&tt2)
+		Equal(t, errs, nil)
+
+		tt2.Inner = NotRed{Color: "blue"}
+
+		errs = validate.Struct(&tt2)
+		Equal(t, errs, nil)
+
+		tt2.Inner = NotRed{Color: "red"}
+		errs = validate.Struct(&tt2)
+		NotEqual(t, errs, nil)
+
+		fe := errs.(ValidationErrors)[0]
+		Equal(t, fe.Field(), "Inner")
+		Equal(t, fe.Namespace(), "Test2.Inner")
+		Equal(t, fe.Tag(), "validateFn")
+	})
+
+	t.Run("using struct with custom function", func(t *testing.T) {
+		validate := New()
+
+		type Test2 struct {
+			String string
+			Inner  NotRed `validate:"validateFn=IsNotRed"`
+		}
+
+		var tt2 Test2
+
+		errs := validate.Struct(&tt2)
+		Equal(t, errs, nil)
+
+		tt2.Inner = NotRed{Color: "blue"}
+
+		errs = validate.Struct(&tt2)
+		Equal(t, errs, nil)
+
+		tt2.Inner = NotRed{Color: "red"}
+		errs = validate.Struct(&tt2)
+		NotEqual(t, errs, nil)
+
+		fe := errs.(ValidationErrors)[0]
+		Equal(t, fe.Field(), "Inner")
+		Equal(t, fe.Namespace(), "Test2.Inner")
+		Equal(t, fe.Tag(), "validateFn")
+	})
+
+	t.Run("try validate method with wrong signature or not existent", func(t *testing.T) {
+		validate := New()
+
+		type Test2 struct {
+			String string `validate:"validateFn=NotExists"` // should fail, method not found
+			Inner  NotRed `validate:"validateFn=DoNothing"` // should fail, return nothing
+			Inner2 NotRed `validate:"validateFn=String"`    // should fail, wrong return (must be error or bool)
+		}
+
+		var tt2 Test2
+
+		err := validate.Struct(&tt2)
+		NotEqual(t, err, nil)
+
+		errs := err.(ValidationErrors)
+
+		Equal(t, len(errs), 3)
+
+		fe := errs[0]
+		Equal(t, fe.Field(), "String")
+		Equal(t, fe.Namespace(), "Test2.String")
+		Equal(t, fe.Tag(), "validateFn")
+
+		fe = errs[1]
+		Equal(t, fe.Field(), "Inner")
+		Equal(t, fe.Namespace(), "Test2.Inner")
+		Equal(t, fe.Tag(), "validateFn")
+
+		fe = errs[2]
+		Equal(t, fe.Field(), "Inner2")
+		Equal(t, fe.Namespace(), "Test2.Inner2")
+		Equal(t, fe.Tag(), "validateFn")
+	})
+}