diff --git a/errors.go b/errors.go index fd906256..0a02b3fd 100644 --- a/errors.go +++ b/errors.go @@ -2,6 +2,7 @@ package validator import ( "bytes" + "errors" "fmt" "reflect" "strings" @@ -50,6 +51,20 @@ func (ve ValidationErrors) Error() string { return strings.TrimSpace(buff.String()) } +// Unwrap returns the nested errors reported by individual field validations. +func (ve ValidationErrors) Unwrap() []error { + errs := make([]error, 0, len(ve)) + for _, fe := range ve { + if err := errors.Unwrap(fe); err != nil { + errs = append(errs, err) + } + } + if len(errs) == 0 { + return nil + } + return errs +} + // Translate translates all of the ValidationErrors func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations { trans := make(ValidationErrorsTranslations) @@ -174,6 +189,7 @@ type fieldError struct { param string kind reflect.Kind typ reflect.Type + err error } // Tag returns the validation tag that failed. @@ -248,6 +264,11 @@ func (fe *fieldError) Error() string { return fmt.Sprintf(fieldErrMsg, fe.ns, fe.Field(), fe.tag) } +// Unwrap returns the underlying error reported by the validation, if any. +func (fe *fieldError) Unwrap() error { + return fe.err +} + // Translate returns the FieldError's translated error // from the provided 'ut.Translator' and registered 'TranslationFunc' // diff --git a/no_validate_fn.go b/no_validate_fn.go index 603af531..09097000 100644 --- a/no_validate_fn.go +++ b/no_validate_fn.go @@ -16,6 +16,9 @@ func isValidateFn(fl FieldLevel) bool { ok, err := tryCallValidateFn(field, validateFn) if err != nil { + if v, ok := fl.(*validate); ok { + v.fieldErr = err + } return false } @@ -48,7 +51,11 @@ func tryCallValidateFn(field reflect.Value, validateFn string) (bool, error) { errorType := reflect.TypeOf((*error)(nil)).Elem() if firstReturnValue.Type().Implements(errorType) { - return firstReturnValue.IsNil(), nil + if firstReturnValue.IsNil() { + return true, nil + } + + return false, firstReturnValue.Interface().(error) } return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got interface %v expect error)", diff --git a/validator.go b/validator.go index 995b0e19..86506be0 100644 --- a/validator.go +++ b/validator.go @@ -17,6 +17,7 @@ type validate struct { errs ValidationErrors includeExclude map[string]struct{} // reset only if StructPartial or StructExcept are called, no need otherwise ffn FilterFunc + fieldErr error slflParent reflect.Value // StructLevel & FieldLevel slCurrent reflect.Value // StructLevel & FieldLevel flField reflect.Value // StructLevel & FieldLevel @@ -462,6 +463,7 @@ OUTER: default: // set Field Level fields + v.fieldErr = nil v.slflParent = parent v.flField = current v.cf = cf @@ -489,6 +491,7 @@ OUTER: param: ct.param, kind: kind, typ: typ, + err: v.fieldErr, }, ) diff --git a/validator_test.go b/validator_test.go index db6750ea..dcd0aec7 100644 --- a/validator_test.go +++ b/validator_test.go @@ -15644,6 +15644,14 @@ func (r NotRed) DoNothing() {} func (r NotRed) String() string { return "not red instance" } +var errValidateFnFailed = errors.New("validate function failed") + +type ValidateFnFailed struct{} + +func (ValidateFnFailed) Validate() error { + return errValidateFnFailed +} + func TestValidateFn(t *testing.T) { t.Run("using pointer", func(t *testing.T) { validate := New() @@ -15733,6 +15741,28 @@ func TestValidateFn(t *testing.T) { Equal(t, fe.Tag(), "validateFn") }) + t.Run("returned error can be unwrapped", func(t *testing.T) { + validate := New() + + type Test struct { + Inner ValidateFnFailed `validate:"validateFn"` + } + + err := validate.Struct(&Test{}) + NotEqual(t, err, nil) + Equal(t, errors.Is(err, errValidateFnFailed), true) + + errs := err.(ValidationErrors) + Equal(t, len(errs), 1) + + fe := errs[0] + Equal(t, fe.Field(), "Inner") + Equal(t, fe.Namespace(), "Test.Inner") + Equal(t, fe.Tag(), "validateFn") + Equal(t, errors.Is(fe, errValidateFnFailed), true) + Equal(t, errors.Unwrap(fe), errValidateFnFailed) + }) + t.Run("try validate method with wrong signature or not existent", func(t *testing.T) { validate := New()