diff --git a/validator.go b/validator.go index d7c2e658..cc80a2dd 100644 --- a/validator.go +++ b/validator.go @@ -28,6 +28,7 @@ type validate struct { fldIsPointer bool // StructLevel & FieldLevel isPartial bool hasExcludes bool + isFailFast bool // Returns on first error encountered } // parent and current will be the same the first run of validateStruct @@ -76,6 +77,10 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur } v.traverseField(ctx, current, current.Field(f.idx), ns, structNs, f, f.cTags) + + if v.isFailFast && len(v.errs) > 0 { + return + } } } diff --git a/validator_instance.go b/validator_instance.go index 779f689a..c994cfa7 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -81,21 +81,22 @@ type internalValidationFuncWrapper struct { // Validate contains the validator settings and cache type Validate struct { - tagName string - pool *sync.Pool - tagNameFunc TagNameFunc - structLevelFuncs map[reflect.Type]StructLevelFuncCtx - customFuncs map[reflect.Type]CustomTypeFunc - aliases map[string]string - validations map[string]internalValidationFuncWrapper - transTagFunc map[ut.Translator]map[string]TranslationFunc // map[]map[]TranslationFunc - rules map[reflect.Type]map[string]string - tagCache *tagCache - structCache *structCache - hasCustomFuncs bool - hasTagNameFunc bool + tagName string + pool *sync.Pool + tagNameFunc TagNameFunc + structLevelFuncs map[reflect.Type]StructLevelFuncCtx + customFuncs map[reflect.Type]CustomTypeFunc + aliases map[string]string + validations map[string]internalValidationFuncWrapper + transTagFunc map[ut.Translator]map[string]TranslationFunc // map[]map[]TranslationFunc + rules map[reflect.Type]map[string]string + tagCache *tagCache + structCache *structCache + hasCustomFuncs bool + hasTagNameFunc bool requiredStructEnabled bool privateFieldValidation bool + isFailFast bool } // New returns a new instance of 'validate' with sane defaults. @@ -156,6 +157,11 @@ func New(options ...Option) *Validate { return v } +// FailFast sets the error response to return the first validation error encountered +func (v *Validate) FailFast() { + v.isFailFast = true +} + // SetTagName allows for changing of the default tag name of 'validate' func (v *Validate) SetTagName(name string) { v.tagName = name @@ -391,6 +397,7 @@ func (v *Validate) StructCtx(ctx context.Context, s interface{}) (err error) { vd := v.pool.Get().(*validate) vd.top = top vd.isPartial = false + vd.isFailFast = v.isFailFast // vd.hasExcludes = false // only need to reset in StructPartial and StructExcept vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil) @@ -437,6 +444,7 @@ func (v *Validate) StructFilteredCtx(ctx context.Context, s interface{}, fn Filt vd.top = top vd.isPartial = true vd.ffn = fn + vd.isFailFast = v.isFailFast // vd.hasExcludes = false // only need to reset in StructPartial and StructExcept vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil) @@ -487,6 +495,7 @@ func (v *Validate) StructPartialCtx(ctx context.Context, s interface{}, fields . vd.ffn = nil vd.hasExcludes = false vd.includeExclude = make(map[string]struct{}) + vd.isFailFast = v.isFailFast typ := val.Type() name := typ.Name() @@ -577,6 +586,7 @@ func (v *Validate) StructExceptCtx(ctx context.Context, s interface{}, fields .. vd.ffn = nil vd.hasExcludes = true vd.includeExclude = make(map[string]struct{}) + vd.isFailFast = v.isFailFast typ := val.Type() name := typ.Name() @@ -648,6 +658,7 @@ func (v *Validate) VarCtx(ctx context.Context, field interface{}, tag string) (e vd := v.pool.Get().(*validate) vd.top = val vd.isPartial = false + vd.isFailFast = v.isFailFast vd.traverseField(ctx, val, val, vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag) if len(vd.errs) > 0 { @@ -700,6 +711,7 @@ func (v *Validate) VarWithValueCtx(ctx context.Context, field interface{}, other vd := v.pool.Get().(*validate) vd.top = otherVal vd.isPartial = false + vd.isFailFast = v.isFailFast vd.traverseField(ctx, otherVal, reflect.ValueOf(field), vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag) if len(vd.errs) > 0 { diff --git a/validator_test.go b/validator_test.go index af05d19d..19625c83 100644 --- a/validator_test.go +++ b/validator_test.go @@ -12080,7 +12080,7 @@ func TestExcludedIf(t *testing.T) { test11 := struct { Field1 bool - Field2 *string `validate:"excluded_if=Field1 false"` + Field2 *string `validate:"excluded_if=Field1 false"` }{ Field1: false, Field2: nil, @@ -13955,6 +13955,122 @@ func TestNestedStructValidation(t *testing.T) { } } +type Value struct { + Street string `validate:"required"` + City string `validate:"required"` +} + +func TestFailFastSettingStruct(t *testing.T) { + + tests := []struct { + v Value + failFast bool + expectedErrLen int + }{ + { + v: Value{ + Street: "", + City: "", + }, + failFast: true, + expectedErrLen: 1, + }, + { + v: Value{ + Street: "", + City: "", + }, + failFast: false, + expectedErrLen: 2, + }, + } + + for _, t := range tests { + validate := New() + if t.failFast { + validate.FailFast() + } + errs := validate.Struct(t.v) + + validationErrs := errs.(ValidationErrors) + IsEqual(len(validationErrs), t.expectedErrLen) + } +} + +func TestFailFastSettingStructPartialCtx(t *testing.T) { + + tests := []struct { + v Value + failFast bool + expectedErrLen int + }{ + { + v: Value{ + Street: "", + City: "", + }, + failFast: true, + expectedErrLen: 1, + }, + { + v: Value{ + Street: "", + City: "", + }, + failFast: false, + expectedErrLen: 2, + }, + } + + for _, t := range tests { + validate := New() + if t.failFast { + validate.FailFast() + } + errs := validate.StructPartialCtx(context.TODO(), t.v, "Street", "City") + + validationErrs := errs.(ValidationErrors) + IsEqual(len(validationErrs), t.expectedErrLen) + } +} + +func TestFailFastSettingStructExceptCtx(t *testing.T) { + + tests := []struct { + v Value + failFast bool + expectedErrLen int + }{ + { + v: Value{ + Street: "", + City: "", + }, + failFast: true, + expectedErrLen: 1, + }, + { + v: Value{ + Street: "", + City: "", + }, + failFast: false, + expectedErrLen: 2, + }, + } + + for _, t := range tests { + validate := New() + if t.failFast { + validate.FailFast() + } + errs := validate.StructPartialCtx(context.TODO(), t.v, "Street", "City") + + validationErrs := errs.(ValidationErrors) + IsEqual(len(validationErrs), t.expectedErrLen) + } +} + func TestTimeRequired(t *testing.T) { validate := New() validate.RegisterTagNameFunc(func(fld reflect.StructField) string {