From d33c84f4da0e5e33db868a357e033f060be959db Mon Sep 17 00:00:00 2001 From: janik-mac Date: Mon, 3 Jul 2023 16:59:00 -0400 Subject: [PATCH 1/5] nested struct validation --- cache.go | 21 +++++--- util.go | 8 +++ validator.go | 2 +- validator_test.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index bbfd2a4a..ddd37b83 100644 --- a/cache.go +++ b/cache.go @@ -20,6 +20,7 @@ const ( typeOr typeKeys typeEndKeys + typeNestedStructLevel ) const ( @@ -152,7 +153,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr // and so only struct level caching can be used instead of combined with Field tag caching if len(tag) > 0 { - ctag, _ = v.parseFieldTagsRecursive(tag, fld.Name, "", false) + ctag, _ = v.parseFieldTagsRecursive(tag, fld, "", false) } else { // even if field doesn't have validations need cTag for traversing to potential inner/nested // elements of the field. @@ -171,7 +172,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr return cs } -func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) { +func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) { var t string noAlias := len(alias) == 0 tags := strings.Split(tag, tagSeparator) @@ -185,9 +186,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s // check map for alias and process new tags, otherwise process as usual if tagsVal, found := v.aliases[t]; found { if i == 0 { - firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true) + firstCtag, current = v.parseFieldTagsRecursive(tagsVal, field, t, true) } else { - next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true) + next, curr := v.parseFieldTagsRecursive(tagsVal, field, t, true) current.next, current = next, curr } @@ -235,7 +236,7 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s } } - current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false) + current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), field, "", false) continue case endKeysTag: @@ -284,14 +285,18 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s current.tag = vals[0] if len(current.tag) == 0 { - panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName))) + panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, field.Name))) } if wrapper, ok := v.validations[current.tag]; ok { current.fn = wrapper.fn current.runValidationWhenNil = wrapper.runValidatinOnNil } else { - panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName))) + panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, field.Name))) + } + + if current.typeof == typeDefault && isNestedStructOrStructPtr(field) { + current.typeof = typeNestedStructLevel } if len(orVals) > 1 { @@ -319,7 +324,7 @@ func (v *Validate) fetchCacheTag(tag string) *cTag { // isn't parsed again. ctag, found = v.tagCache.Get(tag) if !found { - ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false) + ctag, _ = v.parseFieldTagsRecursive(tag, reflect.StructField{}, "", false) v.tagCache.Set(tag, ctag) } } diff --git a/util.go b/util.go index 3925cfe1..b81d9543 100644 --- a/util.go +++ b/util.go @@ -286,3 +286,11 @@ func panicIf(err error) { panic(err.Error()) } } + +func isNestedStructOrStructPtr(v reflect.StructField) bool { + if v.Type == nil { + return false + } + kind := v.Type.Kind() + return kind == reflect.Struct || kind == reflect.Ptr && v.Type.Elem().Kind() == reflect.Struct +} diff --git a/validator.go b/validator.go index 6f6d53ad..a6fa1f5d 100644 --- a/validator.go +++ b/validator.go @@ -170,7 +170,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr if ct.typeof == typeStructOnly { goto CONTINUE - } else if ct.typeof == typeIsDefault { + } else if ct.typeof == typeIsDefault || ct.typeof == typeNestedStructLevel { // set Field Level fields v.slflParent = parent v.flField = current diff --git a/validator_test.go b/validator_test.go index 74f49451..bed2d61f 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13135,3 +13135,124 @@ func TestCronExpressionValidation(t *testing.T) { } } } + +func TestStructTopLevelValidation(t *testing.T) { + type ( + veggyBasket struct { + Root string + Squash string `validate:"required"` + } + testErr struct { + nsKey, + structNsKey, + field, + structField, + expectedTag string + } + ) + + validator := New() + + if err := validator.RegisterValidation("veggy", func(f FieldLevel) bool { + v, ok := f.Field().Interface().(veggyBasket) + if !ok || v.Root != "potato" { + return false + } + return true + }); err != nil { + t.Fatal(fmt.Errorf("failed to register potato tag: %w", err)) + } + + tests := []struct { + name string + testErr *testErr + value veggyBasket + }{ + { + name: "valid", + value: veggyBasket{"potato", "zucchini"}, + }, { + name: "failedVeggyTag", + value: veggyBasket{"zucchini", "potato"}, + testErr: &testErr{ + nsKey: "topLevel.VeggyBasket", + structNsKey: "topLevel.VeggyBasket", + field: "VeggyBasket", + structField: "VeggyBasket", + expectedTag: "veggy", + }, + }, { + name: "failedRequiredTag", + value: veggyBasket{"potato", ""}, + testErr: &testErr{ + nsKey: "topLevel.VeggyBasket.Squash", + structNsKey: "topLevel.VeggyBasket.Squash", + field: "Squash", + structField: "Squash", + expectedTag: "required", + }, + }, { + name: "failedVeggyTagPriorityCheck", + value: veggyBasket{"zucchini", ""}, + testErr: &testErr{ + nsKey: "topLevel.VeggyBasket", + structNsKey: "topLevel.VeggyBasket", + field: "VeggyBasket", + structField: "VeggyBasket", + expectedTag: "veggy", + }, + }, + } + + for _, tt := range tests { + type topLevel struct { + VeggyBasket veggyBasket `validate:"veggy"` + } + + t.Run(tt.name, func(t *testing.T) { + errs := validator.Struct(topLevel{tt.value}) + + if tt.testErr != nil && errs != nil { + AssertError(t, errs, tt.testErr.nsKey, tt.testErr.structNsKey, tt.testErr.field, tt.testErr.structField, tt.testErr.expectedTag) + } + + var validationErrs ValidationErrors + if errs != nil { + validationErrs = errs.(ValidationErrors) + } + + shouldFail := tt.testErr != nil + hasFailed := validationErrs != nil + if shouldFail != hasFailed { + t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, validationErrs) + } + }) + } + + // Also test on struct pointers + for _, tt := range tests { + type topLevel struct { + VeggyBasket *veggyBasket `validate:"veggy"` + } + + t.Run(tt.name+"Ptr", func(t *testing.T) { + errs := validator.Struct(topLevel{&tt.value}) + + t.Log(errs) + if tt.testErr != nil && errs != nil { + AssertError(t, errs, tt.testErr.nsKey, tt.testErr.structNsKey, tt.testErr.field, tt.testErr.structField, tt.testErr.expectedTag) + } + + var validationErrs ValidationErrors + if errs != nil { + validationErrs = errs.(ValidationErrors) + } + + shouldFail := tt.testErr != nil + hasFailed := validationErrs != nil + if shouldFail != hasFailed { + t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, validationErrs) + } + }) + } +} From 5f9d32c38652417fab210ad3579552cd2bcc2207 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Mon, 3 Jul 2023 17:18:14 -0400 Subject: [PATCH 2/5] added test evaluation fn --- validator_test.go | 63 +++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/validator_test.go b/validator_test.go index bed2d61f..fd00e9bd 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13149,6 +13149,11 @@ func TestStructTopLevelValidation(t *testing.T) { structField, expectedTag string } + test struct { + name string + testErr *testErr + value veggyBasket + } ) validator := New() @@ -13163,11 +13168,7 @@ func TestStructTopLevelValidation(t *testing.T) { t.Fatal(fmt.Errorf("failed to register potato tag: %w", err)) } - tests := []struct { - name string - testErr *testErr - value veggyBasket - }{ + tests := []test{ { name: "valid", value: veggyBasket{"potato", "zucchini"}, @@ -13204,28 +13205,30 @@ func TestStructTopLevelValidation(t *testing.T) { }, } + var evaluateTest = func(tt test, errs error) { + if tt.testErr != nil && errs != nil { + AssertError(t, errs, tt.testErr.nsKey, tt.testErr.structNsKey, tt.testErr.field, tt.testErr.structField, tt.testErr.expectedTag) + } + + var validationErrs ValidationErrors + if errs != nil { + validationErrs = errs.(ValidationErrors) + } + + shouldFail := tt.testErr != nil + hasFailed := validationErrs != nil + if shouldFail != hasFailed { + t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, validationErrs) + } + } + for _, tt := range tests { type topLevel struct { VeggyBasket veggyBasket `validate:"veggy"` } t.Run(tt.name, func(t *testing.T) { - errs := validator.Struct(topLevel{tt.value}) - - if tt.testErr != nil && errs != nil { - AssertError(t, errs, tt.testErr.nsKey, tt.testErr.structNsKey, tt.testErr.field, tt.testErr.structField, tt.testErr.expectedTag) - } - - var validationErrs ValidationErrors - if errs != nil { - validationErrs = errs.(ValidationErrors) - } - - shouldFail := tt.testErr != nil - hasFailed := validationErrs != nil - if shouldFail != hasFailed { - t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, validationErrs) - } + evaluateTest(tt, validator.Struct(topLevel{tt.value})) }) } @@ -13236,23 +13239,7 @@ func TestStructTopLevelValidation(t *testing.T) { } t.Run(tt.name+"Ptr", func(t *testing.T) { - errs := validator.Struct(topLevel{&tt.value}) - - t.Log(errs) - if tt.testErr != nil && errs != nil { - AssertError(t, errs, tt.testErr.nsKey, tt.testErr.structNsKey, tt.testErr.field, tt.testErr.structField, tt.testErr.expectedTag) - } - - var validationErrs ValidationErrors - if errs != nil { - validationErrs = errs.(ValidationErrors) - } - - shouldFail := tt.testErr != nil - hasFailed := validationErrs != nil - if shouldFail != hasFailed { - t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, validationErrs) - } + evaluateTest(tt, validator.Struct(topLevel{&tt.value})) }) } } From 02102da08bb2eb2f147cf9d44af0c83b17d4aec4 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Mon, 3 Jul 2023 18:40:55 -0400 Subject: [PATCH 3/5] fix misleading test name --- validator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_test.go b/validator_test.go index fd00e9bd..9b726c5d 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13136,7 +13136,7 @@ func TestCronExpressionValidation(t *testing.T) { } } -func TestStructTopLevelValidation(t *testing.T) { +func TestNestedStructValidation(t *testing.T) { type ( veggyBasket struct { Root string From 79708da0476e0aea954f0f853a3617167b0da9f2 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Tue, 4 Jul 2023 15:43:22 -0400 Subject: [PATCH 4/5] fix uncomparable panic err when using any of the required/excluded tag variants on structs containing uncomparable types such as maps or slices Also added a test to (partially) cover this case. --- baked_in.go | 4 ++-- validator_test.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/baked_in.go b/baked_in.go index e676f1d1..ed0cf3bb 100644 --- a/baked_in.go +++ b/baked_in.go @@ -1710,7 +1710,7 @@ func hasValue(fl FieldLevel) bool { if fl.(*validate).fldIsPointer && field.Interface() != nil { return true } - return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface() + return field.IsValid() && !field.IsZero() } } @@ -1734,7 +1734,7 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo if nullable && field.Interface() != nil { return false } - return field.IsValid() && field.Interface() == reflect.Zero(field.Type()).Interface() + return field.IsValid() && field.IsZero() } } diff --git a/validator_test.go b/validator_test.go index 9b726c5d..aa9c93e3 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13243,3 +13243,19 @@ func TestNestedStructValidation(t *testing.T) { }) } } + +func TestRequiredStruct(t *testing.T) { + type value struct { + Field []string + } + type topLevel struct { + Value value `validate:"required"` + } + + validator := New() + errs := validator.Struct(topLevel{}) + AssertError(t, errs, "topLevel.Value", "topLevel.Value", "Value", "Value", "required") + + errs = validator.Struct(topLevel{value{[]string{}}}) + Equal(t, errs, nil) +} From b43cc52401fb4df932fb933b43f2614a8e97c20d Mon Sep 17 00:00:00 2001 From: janik-mac Date: Sun, 30 Jul 2023 16:54:49 -0400 Subject: [PATCH 5/5] updated docs and added test coverage for nested structs validation --- doc.go | 18 +-- validator_test.go | 331 +++++++++++++++++++++++++++++++++------------- 2 files changed, 251 insertions(+), 98 deletions(-) diff --git a/doc.go b/doc.go index f5aa9e52..84467dcc 100644 --- a/doc.go +++ b/doc.go @@ -247,7 +247,7 @@ Example #2 This validates that the value is not the data types default zero value. For numbers ensures value is not zero. For strings ensures value is not "". For slices, maps, pointers, interfaces, channels and functions -ensures the value is not nil. +ensures the value is not nil. For structs ensures value is not the zero value. Usage: required @@ -256,7 +256,7 @@ ensures the value is not nil. The field under validation must be present and not empty only if all the other specified fields are equal to the value following the specified field. For strings ensures value is not "". For slices, maps, pointers, -interfaces, channels and functions ensures the value is not nil. +interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value. Usage: required_if @@ -273,7 +273,7 @@ Examples: The field under validation must be present and not empty unless all the other specified fields are equal to the value following the specified field. For strings ensures value is not "". For slices, maps, pointers, -interfaces, channels and functions ensures the value is not nil. +interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value. Usage: required_unless @@ -290,7 +290,7 @@ Examples: The field under validation must be present and not empty only if any of the other specified fields are present. For strings ensures value is not "". For slices, maps, pointers, interfaces, channels and functions -ensures the value is not nil. +ensures the value is not nil. For structs ensures value is not the zero value. Usage: required_with @@ -307,7 +307,7 @@ Examples: The field under validation must be present and not empty only if all of the other specified fields are present. For strings ensures value is not "". For slices, maps, pointers, interfaces, channels and functions -ensures the value is not nil. +ensures the value is not nil. For structs ensures value is not the zero value. Usage: required_with_all @@ -321,7 +321,7 @@ Example: The field under validation must be present and not empty only when any of the other specified fields are not present. For strings ensures value is not "". For slices, maps, pointers, interfaces, channels and functions -ensures the value is not nil. +ensures the value is not nil. For structs ensures value is not the zero value. Usage: required_without @@ -338,7 +338,7 @@ Examples: The field under validation must be present and not empty only when all of the other specified fields are not present. For strings ensures value is not "". For slices, maps, pointers, interfaces, channels and functions -ensures the value is not nil. +ensures the value is not nil. For structs ensures value is not the zero value. Usage: required_without_all @@ -352,7 +352,7 @@ Example: The field under validation must not be present or not empty only if all the other specified fields are equal to the value following the specified field. For strings ensures value is not "". For slices, maps, pointers, -interfaces, channels and functions ensures the value is not nil. +interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value. Usage: excluded_if @@ -369,7 +369,7 @@ Examples: The field under validation must not be present or empty unless all the other specified fields are equal to the value following the specified field. For strings ensures value is not "". For slices, maps, pointers, -interfaces, channels and functions ensures the value is not nil. +interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value. Usage: excluded_unless diff --git a/validator_test.go b/validator_test.go index aa9c93e3..8ca2c2aa 100644 --- a/validator_test.go +++ b/validator_test.go @@ -6142,11 +6142,13 @@ func TestNoStructLevelValidation(t *testing.T) { } type Outer struct { - InnerStruct *Inner `validate:"required,nostructlevel"` + InnerStruct Inner `validate:"required,nostructlevel"` + InnerStructPtr *Inner `validate:"required,nostructlevel"` } outer := &Outer{ - InnerStruct: nil, + InnerStructPtr: nil, + InnerStruct: Inner{}, } validate := New() @@ -6154,13 +6156,15 @@ func TestNoStructLevelValidation(t *testing.T) { errs := validate.Struct(outer) NotEqual(t, errs, nil) AssertError(t, errs, "Outer.InnerStruct", "Outer.InnerStruct", "InnerStruct", "InnerStruct", "required") + AssertError(t, errs, "Outer.InnerStructPtr", "Outer.InnerStructPtr", "InnerStructPtr", "InnerStructPtr", "required") - inner := &Inner{ + inner := Inner{ Test: "1234", } outer = &Outer{ - InnerStruct: inner, + InnerStruct: inner, + InnerStructPtr: &inner, } errs = validate.Struct(outer) @@ -6173,11 +6177,13 @@ func TestStructOnlyValidation(t *testing.T) { } type Outer struct { - InnerStruct *Inner `validate:"required,structonly"` + InnerStruct Inner `validate:"required,structonly"` + InnerStructPtr *Inner `validate:"required,structonly"` } outer := &Outer{ - InnerStruct: nil, + InnerStruct: Inner{}, + InnerStructPtr: nil, } validate := New() @@ -6185,13 +6191,15 @@ func TestStructOnlyValidation(t *testing.T) { errs := validate.Struct(outer) NotEqual(t, errs, nil) AssertError(t, errs, "Outer.InnerStruct", "Outer.InnerStruct", "InnerStruct", "InnerStruct", "required") + AssertError(t, errs, "Outer.InnerStructPtr", "Outer.InnerStructPtr", "InnerStructPtr", "InnerStructPtr", "required") - inner := &Inner{ + inner := Inner{ Test: "1234", } outer = &Outer{ - InnerStruct: inner, + InnerStruct: inner, + InnerStructPtr: &inner, } errs = validate.Struct(outer) @@ -10823,6 +10831,8 @@ func TestRequiredIf(t *testing.T) { Field6 uint `validate:"required_if=Field5 1" json:"field_6"` Field7 float32 `validate:"required_if=Field6 1" json:"field_7"` Field8 float64 `validate:"required_if=Field7 1.0" json:"field_8"` + Field9 Inner `validate:"required_if=Field1 test" json:"field_9"` + Field10 *Inner `validate:"required_if=Field1 test" json:"field_10"` }{ Inner: &Inner{Field: &fieldVal}, Field2: &fieldVal, @@ -10848,6 +10858,8 @@ func TestRequiredIf(t *testing.T) { Field5 string `validate:"required_if=Field3 1" json:"field_5"` Field6 string `validate:"required_if=Inner.Field test" json:"field_6"` Field7 string `validate:"required_if=Inner2.Field test" json:"field_7"` + Field8 Inner `validate:"required_if=Field2 test" json:"field_8"` + Field9 *Inner `validate:"required_if=Field2 test" json:"field_9"` }{ Inner: &Inner{Field: &fieldVal}, Field2: &fieldVal, @@ -10857,10 +10869,12 @@ func TestRequiredIf(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 3) + Equal(t, len(ve), 5) AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_if") AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_if") AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_if") + AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_if") + AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_if") defer func() { if r := recover(); r == nil { @@ -10897,6 +10911,8 @@ func TestRequiredUnless(t *testing.T) { Field8 float64 `validate:"required_unless=Field7 0.0" json:"field_8"` Field9 bool `validate:"omitempty" json:"field_9"` Field10 string `validate:"required_unless=Field9 true" json:"field_10"` + Field11 Inner `validate:"required_unless=Field9 true" json:"field_11"` + Field12 *Inner `validate:"required_unless=Field9 true" json:"field_12"` }{ FieldE: "test", Field2: &fieldVal, @@ -10925,6 +10941,8 @@ func TestRequiredUnless(t *testing.T) { Field7 string `validate:"required_unless=Inner2.Field test" json:"field_7"` Field8 bool `validate:"omitempty" json:"field_8"` Field9 string `validate:"required_unless=Field8 true" json:"field_9"` + Field10 Inner `validate:"required_unless=Field9 true" json:"field_10"` + Field11 *Inner `validate:"required_unless=Field9 true" json:"field_11"` }{ Inner: &Inner{Field: &fieldVal}, FieldE: "test", @@ -10935,11 +10953,13 @@ func TestRequiredUnless(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 4) + Equal(t, len(ve), 6) AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_unless") AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_unless") AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_unless") AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_unless") + AssertError(t, errs, "Field10", "Field10", "Field10", "Field10", "required_unless") + AssertError(t, errs, "Field11", "Field11", "Field11", "Field11", "required_unless") defer func() { if r := recover(); r == nil { @@ -10976,6 +10996,8 @@ func TestSkipUnless(t *testing.T) { Field8 float64 `validate:"skip_unless=Field7 1.0" json:"field_8"` Field9 bool `validate:"omitempty" json:"field_9"` Field10 string `validate:"skip_unless=Field9 false" json:"field_10"` + Field11 Inner `validate:"skip_unless=Field9 false" json:"field_11"` + Field12 *Inner `validate:"skip_unless=Field9 false" json:"field_12"` }{ FieldE: "test1", Field2: &fieldVal, @@ -11004,6 +11026,8 @@ func TestSkipUnless(t *testing.T) { Field7 string `validate:"skip_unless=Inner2.Field test" json:"field_7"` Field8 bool `validate:"omitempty" json:"field_8"` Field9 string `validate:"skip_unless=Field8 true" json:"field_9"` + Field10 Inner `validate:"skip_unless=Field8 false" json:"field_10"` + Field11 *Inner `validate:"skip_unless=Field8 false" json:"field_11"` }{ Inner: &Inner{Field: &fieldVal}, FieldE: "test1", @@ -11014,8 +11038,10 @@ func TestSkipUnless(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 1) + Equal(t, len(ve), 3) AssertError(t, errs, "Field5", "Field5", "Field5", "Field5", "skip_unless") + AssertError(t, errs, "Field10", "Field10", "Field10", "Field10", "skip_unless") + AssertError(t, errs, "Field11", "Field11", "Field11", "Field11", "skip_unless") test3 := struct { Inner *Inner @@ -11055,13 +11081,17 @@ func TestRequiredWith(t *testing.T) { Field2 *string `validate:"required_with=Field1" json:"field_2"` Field3 map[string]string `validate:"required_with=Field2" json:"field_3"` Field4 interface{} `validate:"required_with=Field3" json:"field_4"` - Field5 string `validate:"required_with=Inner.Field" json:"field_5"` + Field5 string `validate:"required_with=Field" json:"field_5"` + Field6 Inner `validate:"required_with=Field2" json:"field_6"` + Field7 *Inner `validate:"required_with=Field2" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Field2: &fieldVal, Field3: map[string]string{"key": "val"}, Field4: "test", Field5: "test", + Field6: Inner{Field: &fieldVal}, + Field7: &Inner{Field: &fieldVal}, } validate := New() @@ -11081,6 +11111,8 @@ func TestRequiredWith(t *testing.T) { Field5 string `validate:"required_with=Field3" json:"field_5"` Field6 string `validate:"required_with=Inner.Field" json:"field_6"` Field7 string `validate:"required_with=Inner2.Field" json:"field_7"` + Field8 Inner `validate:"required_with=Field2" json:"field_8"` + Field9 *Inner `validate:"required_with=Field2" json:"field_9"` }{ Inner: &Inner{Field: &fieldVal}, Field2: &fieldVal, @@ -11090,10 +11122,12 @@ func TestRequiredWith(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 3) + Equal(t, len(ve), 5) AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_with") AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_with") AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_with") + AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_with") + AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_with") } func TestExcludedWith(t *testing.T) { @@ -11114,6 +11148,8 @@ func TestExcludedWith(t *testing.T) { Field4 interface{} `validate:"excluded_with=FieldE" json:"field_4"` Field5 string `validate:"excluded_with=Inner.FieldE" json:"field_5"` Field6 string `validate:"excluded_with=Inner2.FieldE" json:"field_6"` + Field7 Inner `validate:"excluded_with=FieldE" json:"field_7"` + Field8 *Inner `validate:"excluded_with=FieldE" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field1: fieldVal, @@ -11140,6 +11176,8 @@ func TestExcludedWith(t *testing.T) { Field4 interface{} `validate:"excluded_with=Field" json:"field_4"` Field5 string `validate:"excluded_with=Inner.Field" json:"field_5"` Field6 string `validate:"excluded_with=Inner2.Field" json:"field_6"` + Field7 Inner `validate:"excluded_with=Field" json:"field_7"` + Field8 *Inner `validate:"excluded_with=Field" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field: "populated", @@ -11149,13 +11187,15 @@ func TestExcludedWith(t *testing.T) { Field4: "test", Field5: "test", Field6: "test", + Field7: Inner{FieldE: "potato"}, + Field8: &Inner{FieldE: "potato"}, } errs = validate.Struct(test2) NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 5) + Equal(t, len(ve), 7) for i := 1; i <= 5; i++ { name := fmt.Sprintf("Field%d", i) AssertError(t, errs, name, name, name, name, "excluded_with") @@ -11172,6 +11212,8 @@ func TestExcludedWith(t *testing.T) { Field4 interface{} `validate:"excluded_with=FieldE" json:"field_4"` Field5 string `validate:"excluded_with=Inner.FieldE" json:"field_5"` Field6 string `validate:"excluded_with=Inner2.FieldE" json:"field_6"` + Field7 Inner `validate:"excluded_with=FieldE" json:"field_7"` + Field8 *Inner `validate:"excluded_with=FieldE" json:"field_8"` }{ Inner: &Inner{FieldE: "populated"}, Inner2: &Inner{FieldE: "populated"}, @@ -11201,6 +11243,8 @@ func TestExcludedWithout(t *testing.T) { Field3 map[string]string `validate:"excluded_without=Field" json:"field_3"` Field4 interface{} `validate:"excluded_without=Field" json:"field_4"` Field5 string `validate:"excluded_without=Inner.Field" json:"field_5"` + Field6 Inner `validate:"excluded_without=Field" json:"field_6"` + Field7 *Inner `validate:"excluded_without=Field" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Field: "populated", @@ -11209,6 +11253,8 @@ func TestExcludedWithout(t *testing.T) { Field3: map[string]string{"key": "val"}, Field4: "test", Field5: "test", + Field6: Inner{FieldE: "potato"}, + Field7: &Inner{FieldE: "potato"}, } validate := New() @@ -11227,6 +11273,8 @@ func TestExcludedWithout(t *testing.T) { Field4 interface{} `validate:"excluded_without=FieldE" json:"field_4"` Field5 string `validate:"excluded_without=Inner.FieldE" json:"field_5"` Field6 string `validate:"excluded_without=Inner2.FieldE" json:"field_6"` + Field7 Inner `validate:"excluded_without=FieldE" json:"field_7"` + Field8 *Inner `validate:"excluded_without=FieldE" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field1: fieldVal, @@ -11235,13 +11283,15 @@ func TestExcludedWithout(t *testing.T) { Field4: "test", Field5: "test", Field6: "test", + Field7: Inner{FieldE: "potato"}, + Field8: &Inner{FieldE: "potato"}, } errs = validate.Struct(test2) NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 6) + Equal(t, len(ve), 8) for i := 1; i <= 6; i++ { name := fmt.Sprintf("Field%d", i) AssertError(t, errs, name, name, name, name, "excluded_without") @@ -11257,6 +11307,8 @@ func TestExcludedWithout(t *testing.T) { Field3 map[string]string `validate:"excluded_without=Field" json:"field_3"` Field4 interface{} `validate:"excluded_without=Field" json:"field_4"` Field5 string `validate:"excluded_without=Inner.Field" json:"field_5"` + Field6 Inner `validate:"excluded_without=Field" json:"field_6"` + Field7 *Inner `validate:"excluded_without=Field" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Field: "populated", @@ -11286,6 +11338,8 @@ func TestExcludedWithAll(t *testing.T) { Field4 interface{} `validate:"excluded_with_all=FieldE Field" json:"field_4"` Field5 string `validate:"excluded_with_all=Inner.FieldE" json:"field_5"` Field6 string `validate:"excluded_with_all=Inner2.FieldE" json:"field_6"` + Field7 Inner `validate:"excluded_with_all=FieldE Field" json:"field_7"` + Field8 *Inner `validate:"excluded_with_all=FieldE Field" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field: fieldVal, @@ -11295,6 +11349,8 @@ func TestExcludedWithAll(t *testing.T) { Field4: "test", Field5: "test", Field6: "test", + Field7: Inner{FieldE: "potato"}, + Field8: &Inner{FieldE: "potato"}, } validate := New() @@ -11313,6 +11369,8 @@ func TestExcludedWithAll(t *testing.T) { Field4 interface{} `validate:"excluded_with_all=Field FieldE" json:"field_4"` Field5 string `validate:"excluded_with_all=Inner.Field" json:"field_5"` Field6 string `validate:"excluded_with_all=Inner2.Field" json:"field_6"` + Field7 Inner `validate:"excluded_with_all=Field FieldE" json:"field_7"` + Field8 *Inner `validate:"excluded_with_all=Field FieldE" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field: "populated", @@ -11323,13 +11381,15 @@ func TestExcludedWithAll(t *testing.T) { Field4: "test", Field5: "test", Field6: "test", + Field7: Inner{FieldE: "potato"}, + Field8: &Inner{FieldE: "potato"}, } errs = validate.Struct(test2) NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 5) + Equal(t, len(ve), 7) for i := 1; i <= 5; i++ { name := fmt.Sprintf("Field%d", i) AssertError(t, errs, name, name, name, name, "excluded_with_all") @@ -11346,6 +11406,8 @@ func TestExcludedWithAll(t *testing.T) { Field4 interface{} `validate:"excluded_with_all=FieldE Field" json:"field_4"` Field5 string `validate:"excluded_with_all=Inner.FieldE" json:"field_5"` Field6 string `validate:"excluded_with_all=Inner2.FieldE" json:"field_6"` + Field7 Inner `validate:"excluded_with_all=Field FieldE" json:"field_7"` + Field8 *Inner `validate:"excluded_with_all=Field FieldE" json:"field_8"` }{ Inner: &Inner{FieldE: "populated"}, Inner2: &Inner{FieldE: "populated"}, @@ -11376,6 +11438,8 @@ func TestExcludedWithoutAll(t *testing.T) { Field3 map[string]string `validate:"excluded_without_all=Field FieldE" json:"field_3"` Field4 interface{} `validate:"excluded_without_all=Field FieldE" json:"field_4"` Field5 string `validate:"excluded_without_all=Inner.Field Inner2.Field" json:"field_5"` + Field6 Inner `validate:"excluded_without_all=Field FieldE" json:"field_6"` + Field7 *Inner `validate:"excluded_without_all=Field FieldE" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Inner2: &Inner{Field: &fieldVal}, @@ -11385,6 +11449,8 @@ func TestExcludedWithoutAll(t *testing.T) { Field3: map[string]string{"key": "val"}, Field4: "test", Field5: "test", + Field6: Inner{FieldE: "potato"}, + Field7: &Inner{FieldE: "potato"}, } validate := New() @@ -11403,6 +11469,8 @@ func TestExcludedWithoutAll(t *testing.T) { Field4 interface{} `validate:"excluded_without_all=FieldE Field" json:"field_4"` Field5 string `validate:"excluded_without_all=Inner.FieldE" json:"field_5"` Field6 string `validate:"excluded_without_all=Inner2.FieldE" json:"field_6"` + Field7 Inner `validate:"excluded_without_all=Field FieldE" json:"field_7"` + Field8 *Inner `validate:"excluded_without_all=Field FieldE" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field1: fieldVal, @@ -11411,13 +11479,15 @@ func TestExcludedWithoutAll(t *testing.T) { Field4: "test", Field5: "test", Field6: "test", + Field7: Inner{FieldE: "potato"}, + Field8: &Inner{FieldE: "potato"}, } errs = validate.Struct(test2) NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 6) + Equal(t, len(ve), 8) for i := 1; i <= 6; i++ { name := fmt.Sprintf("Field%d", i) AssertError(t, errs, name, name, name, name, "excluded_without_all") @@ -11433,6 +11503,8 @@ func TestExcludedWithoutAll(t *testing.T) { Field3 map[string]string `validate:"excluded_without_all=Field FieldE" json:"field_3"` Field4 interface{} `validate:"excluded_without_all=Field FieldE" json:"field_4"` Field5 string `validate:"excluded_without_all=Inner.Field Inner2.Field" json:"field_5"` + Field6 Inner `validate:"excluded_without_all=Field FieldE" json:"field_6"` + Field7 *Inner `validate:"excluded_without_all=Field FieldE" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Inner2: &Inner{Field: &fieldVal}, @@ -11461,6 +11533,8 @@ func TestRequiredWithAll(t *testing.T) { Field3 map[string]string `validate:"required_with_all=Field2" json:"field_3"` Field4 interface{} `validate:"required_with_all=Field3" json:"field_4"` Field5 string `validate:"required_with_all=Inner.Field" json:"field_5"` + Field6 Inner `validate:"required_with_all=Field1 Field2" json:"field_6"` + Field7 *Inner `validate:"required_with_all=Field1 Field2" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Field1: "test_field1", @@ -11468,6 +11542,8 @@ func TestRequiredWithAll(t *testing.T) { Field3: map[string]string{"key": "val"}, Field4: "test", Field5: "test", + Field6: Inner{Field: &fieldVal}, + Field7: &Inner{Field: &fieldVal}, } validate := New() @@ -11486,6 +11562,8 @@ func TestRequiredWithAll(t *testing.T) { Field4 interface{} `validate:"required_with_all=Field1 FieldE" json:"field_4"` Field5 string `validate:"required_with_all=Inner.Field Field2" json:"field_5"` Field6 string `validate:"required_with_all=Inner2.Field Field2" json:"field_6"` + Field7 Inner `validate:"required_with_all=Inner.Field Field2" json:"field_7"` + Field8 *Inner `validate:"required_with_all=Inner.Field Field2" json:"field_8"` }{ Inner: &Inner{Field: &fieldVal}, Field2: &fieldVal, @@ -11495,9 +11573,11 @@ func TestRequiredWithAll(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 2) + Equal(t, len(ve), 4) AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_with_all") AssertError(t, errs, "Field5", "Field5", "Field5", "Field5", "required_with_all") + AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_with_all") + AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_with_all") } func TestRequiredWithout(t *testing.T) { @@ -11513,12 +11593,16 @@ func TestRequiredWithout(t *testing.T) { Field3 map[string]string `validate:"required_without=Field2" json:"field_3"` Field4 interface{} `validate:"required_without=Field3" json:"field_4"` Field5 string `validate:"required_without=Field3" json:"field_5"` + Field6 Inner `validate:"required_without=Field1" json:"field_6"` + Field7 *Inner `validate:"required_without=Field1" json:"field_7"` }{ Inner: &Inner{Field: &fieldVal}, Field2: &fieldVal, Field3: map[string]string{"key": "val"}, Field4: "test", Field5: "test", + Field6: Inner{Field: &fieldVal}, + Field7: &Inner{Field: &fieldVal}, } validate := New() @@ -11527,16 +11611,18 @@ func TestRequiredWithout(t *testing.T) { Equal(t, errs, nil) test2 := struct { - Inner *Inner - Inner2 *Inner - Field1 string `json:"field_1"` - Field2 *string `validate:"required_without=Field1" json:"field_2"` - Field3 map[string]string `validate:"required_without=Field2" json:"field_3"` - Field4 interface{} `validate:"required_without=Field3" json:"field_4"` - Field5 string `validate:"required_without=Field3" json:"field_5"` - Field6 string `validate:"required_without=Field1" json:"field_6"` - Field7 string `validate:"required_without=Inner.Field" json:"field_7"` - Field8 string `validate:"required_without=Inner.Field" json:"field_8"` + Inner *Inner + Inner2 *Inner + Field1 string `json:"field_1"` + Field2 *string `validate:"required_without=Field1" json:"field_2"` + Field3 map[string]string `validate:"required_without=Field2" json:"field_3"` + Field4 interface{} `validate:"required_without=Field3" json:"field_4"` + Field5 string `validate:"required_without=Field3" json:"field_5"` + Field6 string `validate:"required_without=Field1" json:"field_6"` + Field7 string `validate:"required_without=Inner.Field" json:"field_7"` + Field8 string `validate:"required_without=Inner.Field" json:"field_8"` + Field9 Inner `validate:"required_without=Field1" json:"field_9"` + Field10 *Inner `validate:"required_without=Field1" json:"field_10"` }{ Inner: &Inner{}, Field3: map[string]string{"key": "val"}, @@ -11548,11 +11634,13 @@ func TestRequiredWithout(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 4) + Equal(t, len(ve), 6) AssertError(t, errs, "Field2", "Field2", "Field2", "Field2", "required_without") AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_without") AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_without") AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_without") + AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_without") + AssertError(t, errs, "Field10", "Field10", "Field10", "Field10", "required_without") test3 := struct { Field1 *string `validate:"required_without=Field2,omitempty,min=1" json:"field_1"` @@ -11566,6 +11654,10 @@ func TestRequiredWithout(t *testing.T) { } func TestRequiredWithoutAll(t *testing.T) { + type nested struct { + value string + } + fieldVal := "test" test := struct { Field1 string `validate:"omitempty" json:"field_1"` @@ -11573,12 +11665,16 @@ func TestRequiredWithoutAll(t *testing.T) { Field3 map[string]string `validate:"required_without_all=Field2" json:"field_3"` Field4 interface{} `validate:"required_without_all=Field3" json:"field_4"` Field5 string `validate:"required_without_all=Field3" json:"field_5"` + Field6 nested `validate:"required_without_all=Field1" json:"field_6"` + Field7 *nested `validate:"required_without_all=Field1" json:"field_7"` }{ Field1: "", Field2: &fieldVal, Field3: map[string]string{"key": "val"}, Field4: "test", Field5: "test", + Field6: nested{"potato"}, + Field7: &nested{"potato"}, } validate := New() @@ -11593,6 +11689,8 @@ func TestRequiredWithoutAll(t *testing.T) { Field4 interface{} `validate:"required_without_all=Field3" json:"field_4"` Field5 string `validate:"required_without_all=Field3" json:"field_5"` Field6 string `validate:"required_without_all=Field1 Field3" json:"field_6"` + Field7 nested `validate:"required_without_all=Field1" json:"field_7"` + Field8 *nested `validate:"required_without_all=Field1" json:"field_8"` }{ Field3: map[string]string{"key": "val"}, Field4: "test", @@ -11603,8 +11701,10 @@ func TestRequiredWithoutAll(t *testing.T) { NotEqual(t, errs, nil) ve := errs.(ValidationErrors) - Equal(t, len(ve), 1) + Equal(t, len(ve), 3) AssertError(t, errs, "Field2", "Field2", "Field2", "Field2", "required_without_all") + AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_without_all") + AssertError(t, errs, "Field8", "Field8", "Field8", "Field8", "required_without_all") } func TestExcludedIf(t *testing.T) { @@ -13137,27 +13237,115 @@ func TestCronExpressionValidation(t *testing.T) { } func TestNestedStructValidation(t *testing.T) { + validator := New() + + t.Run("required", func(t *testing.T) { + type ( + value struct { + Field string + } + topLevel struct { + Nested value `validate:"required"` + } + ) + + var validationErrs ValidationErrors + if errs := validator.Struct(topLevel{}); errs != nil { + validationErrs = errs.(ValidationErrors) + } + + Equal(t, 1, len(validationErrs)) + AssertError(t, validationErrs, "topLevel.Nested", "topLevel.Nested", "Nested", "Nested", "required") + + Equal(t, validator.Struct(topLevel{value{"potato"}}), nil) + }) + + t.Run("omitempty", func(t *testing.T) { + type ( + value struct { + Field string + } + topLevel struct { + Nested value `validate:"omitempty,required"` + } + ) + + errs := validator.Struct(topLevel{}) + Equal(t, errs, nil) + }) + + t.Run("excluded_if", func(t *testing.T) { + type ( + value struct { + Field string + } + topLevel struct { + Field string + Nested value `validate:"excluded_if=Field potato"` + } + ) + + errs := validator.Struct(topLevel{Field: "test", Nested: value{"potato"}}) + Equal(t, errs, nil) + + errs = validator.Struct(topLevel{Field: "potato"}) + Equal(t, errs, nil) + + errs = validator.Struct(topLevel{Field: "potato", Nested: value{"potato"}}) + AssertError(t, errs, "topLevel.Nested", "topLevel.Nested", "Nested", "Nested", "excluded_if") + }) + + t.Run("excluded_unless", func(t *testing.T) { + type ( + value struct { + Field string + } + topLevel struct { + Field string + Nested value `validate:"excluded_unless=Field potato"` + } + ) + + errs := validator.Struct(topLevel{Field: "test"}) + Equal(t, errs, nil) + + errs = validator.Struct(topLevel{Field: "potato", Nested: value{"potato"}}) + Equal(t, errs, nil) + + errs = validator.Struct(topLevel{Field: "test", Nested: value{"potato"}}) + AssertError(t, errs, "topLevel.Nested", "topLevel.Nested", "Nested", "Nested", "excluded_unless") + }) + + t.Run("nonComparableField", func(t *testing.T) { + type ( + value struct { + Field []string + } + topLevel struct { + Nested value `validate:"required"` + } + ) + + errs := validator.Struct(topLevel{value{[]string{}}}) + Equal(t, errs, nil) + }) + type ( veggyBasket struct { Root string Squash string `validate:"required"` } testErr struct { - nsKey, - structNsKey, - field, - structField, - expectedTag string + path string + tag string } test struct { - name string - testErr *testErr - value veggyBasket + name string + err testErr + value veggyBasket } ) - validator := New() - if err := validator.RegisterValidation("veggy", func(f FieldLevel) bool { v, ok := f.Field().Interface().(veggyBasket) if !ok || v.Root != "potato" { @@ -13173,52 +13361,33 @@ func TestNestedStructValidation(t *testing.T) { name: "valid", value: veggyBasket{"potato", "zucchini"}, }, { - name: "failedVeggyTag", + name: "failedCustomTag", value: veggyBasket{"zucchini", "potato"}, - testErr: &testErr{ - nsKey: "topLevel.VeggyBasket", - structNsKey: "topLevel.VeggyBasket", - field: "VeggyBasket", - structField: "VeggyBasket", - expectedTag: "veggy", - }, + err: testErr{"topLevel.VeggyBasket", "veggy"}, }, { - name: "failedRequiredTag", + name: "failedInnerField", value: veggyBasket{"potato", ""}, - testErr: &testErr{ - nsKey: "topLevel.VeggyBasket.Squash", - structNsKey: "topLevel.VeggyBasket.Squash", - field: "Squash", - structField: "Squash", - expectedTag: "required", - }, + err: testErr{"topLevel.VeggyBasket.Squash", "required"}, }, { - name: "failedVeggyTagPriorityCheck", + name: "customTagFailurePriorityCheck", value: veggyBasket{"zucchini", ""}, - testErr: &testErr{ - nsKey: "topLevel.VeggyBasket", - structNsKey: "topLevel.VeggyBasket", - field: "VeggyBasket", - structField: "VeggyBasket", - expectedTag: "veggy", - }, + err: testErr{"topLevel.VeggyBasket", "veggy"}, }, } var evaluateTest = func(tt test, errs error) { - if tt.testErr != nil && errs != nil { - AssertError(t, errs, tt.testErr.nsKey, tt.testErr.structNsKey, tt.testErr.field, tt.testErr.structField, tt.testErr.expectedTag) - } + if tt.err != (testErr{}) && errs != nil { + Equal(t, len(errs.(ValidationErrors)), 1) - var validationErrs ValidationErrors - if errs != nil { - validationErrs = errs.(ValidationErrors) + segments := strings.Split(tt.err.path, ".") + fieldName := segments[len(segments)-1] + AssertError(t, errs, tt.err.path, tt.err.path, fieldName, fieldName, tt.err.tag) } - shouldFail := tt.testErr != nil - hasFailed := validationErrs != nil + shouldFail := tt.err != (testErr{}) + hasFailed := errs != nil if shouldFail != hasFailed { - t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, validationErrs) + t.Fatalf("expected failure %v, got: %v with errs: %v", shouldFail, hasFailed, errs) } } @@ -13243,19 +13412,3 @@ func TestNestedStructValidation(t *testing.T) { }) } } - -func TestRequiredStruct(t *testing.T) { - type value struct { - Field []string - } - type topLevel struct { - Value value `validate:"required"` - } - - validator := New() - errs := validator.Struct(topLevel{}) - AssertError(t, errs, "topLevel.Value", "topLevel.Value", "Value", "Value", "required") - - errs = validator.Struct(topLevel{value{[]string{}}}) - Equal(t, errs, nil) -}