Skip to content

Commit dfc7ccd

Browse files
Fix: support validation for map values with struct types (#1433)
## Fixes Fixed support for validating struct values inside map fields. Previously, if no tag followed `endkeys`, the struct values were skipped. This PR ensures such struct values are still validated according to their own tags. ### Enhancements to validation logic: * Updated `validator.go` to handle map values that are structs or pointers to structs by diving into their fields for validation. * Added test cases covering struct validation in maps, including basic rules, pointer structs, nested structs, and edge cases like nil pointers. Check #1430 for more details. **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
1 parent 5b9542b commit dfc7ccd

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed

validator.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,18 @@ OUTER:
337337
// can be nil when just keys being validated
338338
if ct.next != nil {
339339
v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct.next)
340+
} else {
341+
// Struct fallback when map values are structs
342+
val := current.MapIndex(key)
343+
switch val.Kind() {
344+
case reflect.Ptr:
345+
if val.Elem().Kind() == reflect.Struct {
346+
// Dive into the struct so its own tags run
347+
v.traverseField(ctx, parent, val, ns, structNs, reusableCF, nil)
348+
}
349+
case reflect.Struct:
350+
v.traverseField(ctx, parent, val, ns, structNs, reusableCF, nil)
351+
}
340352
}
341353
} else {
342354
v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct)

validator_test.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14335,3 +14335,301 @@ func TestValidateFn(t *testing.T) {
1433514335
Equal(t, fe.Tag(), "validateFn")
1433614336
})
1433714337
}
14338+
func TestMapStructBasicValidation(t *testing.T) {
14339+
// Tests basic validation of a map with struct values
14340+
type Inner struct {
14341+
Value string `validate:"max=5"`
14342+
}
14343+
type Outer struct {
14344+
Data map[string]Inner `validate:"dive"`
14345+
}
14346+
obj := Outer{
14347+
Data: map[string]Inner{
14348+
"key1": {Value: "exceeds"}, // Should fail because Value is longer than 5 characters
14349+
},
14350+
}
14351+
validate := New()
14352+
errs := validate.Struct(obj)
14353+
NotEqual(t, errs, nil)
14354+
}
14355+
func TestMapStructPointerValidation(t *testing.T) {
14356+
// Tests validation of a map with pointer to struct values
14357+
type Inner struct {
14358+
Count int `validate:"gt=10"`
14359+
}
14360+
type Outer struct {
14361+
Items map[string]*Inner `validate:"dive"`
14362+
}
14363+
obj := Outer{
14364+
Items: map[string]*Inner{
14365+
"a": {Count: 5},
14366+
},
14367+
}
14368+
validate := New()
14369+
errs := validate.Struct(obj)
14370+
NotEqual(t, errs, nil)
14371+
}
14372+
func TestMapStructWithKeyValidationOnly(t *testing.T) {
14373+
// Tests validation of a map with struct values, focusing on key validation
14374+
type Inner struct {
14375+
Name string `validate:"required"`
14376+
}
14377+
type Outer struct {
14378+
Things map[string]Inner `validate:"dive,keys,min=3,endkeys"`
14379+
}
14380+
obj := Outer{
14381+
Things: map[string]Inner{
14382+
"ab": {Name: "valid"}, //Should fail because key is too short
14383+
},
14384+
}
14385+
validate := New()
14386+
errs := validate.Struct(obj)
14387+
NotEqual(t, errs, nil)
14388+
}
14389+
func TestMapStructWithKeyAndValueValidation(t *testing.T) {
14390+
// Tests validation of a map with struct values, validating both keys and values
14391+
type Inner struct {
14392+
Name string `validate:"min=3"`
14393+
}
14394+
type Outer struct {
14395+
Stuff map[string]Inner `validate:"dive,keys,min=2,endkeys"`
14396+
}
14397+
obj := Outer{
14398+
Stuff: map[string]Inner{
14399+
"ok": {Name: "xy"},
14400+
"bad": {Name: "valid"},
14401+
},
14402+
}
14403+
validate := New()
14404+
errs := validate.Struct(obj)
14405+
NotEqual(t, errs, nil)
14406+
}
14407+
func TestMapPointerStructWithNilValue(t *testing.T) {
14408+
// Tests validation of a map with pointer to struct values where value is nil
14409+
type Inner struct {
14410+
Count int `validate:"min=1"`
14411+
}
14412+
type Outer struct {
14413+
Items map[string]*Inner `validate:"dive"`
14414+
}
14415+
obj := Outer{
14416+
Items: map[string]*Inner{
14417+
"x": nil,
14418+
},
14419+
}
14420+
validate := New()
14421+
errs := validate.Struct(obj)
14422+
Equal(t, errs, nil)
14423+
}
14424+
func TestThreeLevelNestedStructs(t *testing.T) {
14425+
// Tests validation of three levels of nested structs with map keys and values
14426+
type Level3 struct {
14427+
Code string `validate:"len=3"`
14428+
}
14429+
14430+
type Level2 struct {
14431+
Items map[string]Level3 `validate:"dive,keys,required,endkeys"`
14432+
}
14433+
14434+
type Level1 struct {
14435+
Levels map[string]Level2 `validate:"dive,keys,required,endkeys"`
14436+
}
14437+
14438+
validate := New()
14439+
14440+
// Valid case: all Level3.Code are exactly 3 chars
14441+
valid := Level1{Levels: map[string]Level2{
14442+
"first": {Items: map[string]Level3{
14443+
"item1": {Code: "abc"},
14444+
"item2": {Code: "xyz"},
14445+
},
14446+
},
14447+
},
14448+
}
14449+
14450+
errs := validate.Struct(valid)
14451+
Equal(t, errs, nil)
14452+
14453+
// Invalid case: one Level3.Code is wrong length
14454+
invalid := Level1{Levels: map[string]Level2{
14455+
"first": {Items: map[string]Level3{
14456+
"item1": {Code: "abcd"}, // Should fail here because length is 4
14457+
"item2": {Code: "xyz"},
14458+
},
14459+
},
14460+
},
14461+
}
14462+
14463+
errs = validate.Struct(invalid)
14464+
NotEqual(t, errs, nil)
14465+
}
14466+
func TestMapStructFallbackWithKeysOnly(t *testing.T) {
14467+
// Tests fallback behavior when validating map keys and struct values
14468+
type Inner struct {
14469+
Value string `validate:"max=3"`
14470+
}
14471+
type Outer struct {
14472+
Data map[string]Inner `validate:"dive,keys,max=2,endkeys"`
14473+
}
14474+
validate := New()
14475+
14476+
// Key too long: should fail on key before fallback
14477+
obj1 := Outer{Data: map[string]Inner{"toolong": {Value: "ok"}}}
14478+
errs := validate.Struct(obj1)
14479+
NotEqual(t, errs, nil)
14480+
14481+
// Key OK, value too long: should fallback and fail on Inner.Value
14482+
obj2 := Outer{Data: map[string]Inner{"ok": {Value: "toolong"}}}
14483+
errs = validate.Struct(obj2)
14484+
NotEqual(t, errs, nil)
14485+
14486+
// Both key and value OK: should pass
14487+
obj3 := Outer{Data: map[string]Inner{"ok": {Value: "abc"}}}
14488+
errs = validate.Struct(obj3)
14489+
Equal(t, errs, nil)
14490+
}
14491+
14492+
func TestMapPointerStructFallback(t *testing.T) {
14493+
// Tests fallback behavior when validating map keys and pointer to struct values
14494+
type Inner struct {
14495+
Count int `validate:"gt=0"`
14496+
}
14497+
type Outer struct {
14498+
Data map[string]*Inner `validate:"dive,keys,max=3,endkeys"`
14499+
}
14500+
validate := New()
14501+
14502+
// Key OK, pointer is nil: no fallback error
14503+
obj1 := Outer{Data: map[string]*Inner{"ok": nil}}
14504+
errs := validate.Struct(obj1)
14505+
Equal(t, errs, nil)
14506+
14507+
// Key OK, pointer non-nil but field invalid: fallback should validate Inner.Count
14508+
obj2 := Outer{Data: map[string]*Inner{"ok": {Count: 0}}}
14509+
errs = validate.Struct(obj2)
14510+
NotEqual(t, errs, nil)
14511+
14512+
// Key OK, pointer non-nil and valid: should pass
14513+
obj3 := Outer{Data: map[string]*Inner{"ok": {Count: 5}}}
14514+
errs = validate.Struct(obj3)
14515+
Equal(t, errs, nil)
14516+
}
14517+
14518+
func TestMapNonStructValueSkipsFallback(t *testing.T) {
14519+
// Tests that fallback is skipped for non-struct values in maps
14520+
type Outer struct {
14521+
Data map[string]int `validate:"dive,keys,min=1,endkeys"`
14522+
}
14523+
validate := New()
14524+
14525+
// Key OK, value is primitive: no fallback needed, should pass
14526+
obj1 := Outer{Data: map[string]int{"a": 0}}
14527+
errs := validate.Struct(obj1)
14528+
Equal(t, errs, nil)
14529+
14530+
// Key too short: should fail on key
14531+
obj2 := Outer{Data: map[string]int{"": 0}}
14532+
errs = validate.Struct(obj2)
14533+
NotEqual(t, errs, nil)
14534+
}
14535+
14536+
func TestMapSliceValueNoFallback(t *testing.T) {
14537+
// Tests that fallback is skipped for slice values in maps
14538+
type Outer struct {
14539+
Data map[string][]string `validate:"dive,keys,max=1,endkeys"`
14540+
}
14541+
validate := New()
14542+
14543+
// Key OK, value is slice: skip fallback, should pass
14544+
obj1 := Outer{Data: map[string][]string{"a": {"x", "y"}}}
14545+
errs := validate.Struct(obj1)
14546+
Equal(t, errs, nil)
14547+
14548+
// Key too long: should fail on key
14549+
obj2 := Outer{Data: map[string][]string{"ab": {"x"}}}
14550+
errs = validate.Struct(obj2)
14551+
NotEqual(t, errs, nil)
14552+
}
14553+
func TestMapEmptyStructValueNoError(t *testing.T) {
14554+
// Tests that empty struct values in maps do not trigger validation errors
14555+
type Inner struct{} // no validation tags
14556+
14557+
type Outer struct {
14558+
Data map[string]Inner `validate:"dive,keys,max=3,endkeys"`
14559+
}
14560+
validate := New()
14561+
14562+
// Key OK, value is empty struct: no fields to validate, should pass
14563+
obj1 := Outer{Data: map[string]Inner{"ok": {}}}
14564+
errs := validate.Struct(obj1)
14565+
Equal(t, errs, nil)
14566+
14567+
// Key too long: should fail on key, skip struct
14568+
obj2 := Outer{Data: map[string]Inner{"toolong": {}}}
14569+
errs = validate.Struct(obj2)
14570+
NotEqual(t, errs, nil)
14571+
}
14572+
14573+
func TestMapNestedEmptyStructs(t *testing.T) {
14574+
// Tests that nested empty structs in maps validate correctly
14575+
type Level3 struct{}
14576+
type Level2 struct {
14577+
Items map[string]Level3 `validate:"dive,keys,max=2,endkeys"`
14578+
}
14579+
type Level1 struct {
14580+
Levels map[string]Level2 `validate:"dive,keys,max=2,endkeys"`
14581+
}
14582+
14583+
validate := New()
14584+
14585+
// All keys within max=2, and inner structs are empty → should pass
14586+
obj := Level1{Levels: map[string]Level2{
14587+
"a": {Items: map[string]Level3{"b": {}}},
14588+
}}
14589+
errs := validate.Struct(obj)
14590+
Equal(t, errs, nil)
14591+
14592+
// Top-level key too long: should fail on Level1 key, skip deeper
14593+
obj2 := Level1{Levels: map[string]Level2{
14594+
"too": {Items: map[string]Level3{"b": {}}}, // "too" length=3 > max=2
14595+
}}
14596+
errs = validate.Struct(obj2)
14597+
NotEqual(t, errs, nil)
14598+
}
14599+
14600+
func TestMapEmptyValueMap(t *testing.T) {
14601+
// Tests that an empty map with struct values does not trigger validation errors
14602+
type Inner struct {
14603+
Value string `validate:"required"`
14604+
}
14605+
14606+
type Outer struct {
14607+
Data map[string]Inner `validate:"dive,keys,max=3,endkeys"`
14608+
}
14609+
validate := New()
14610+
14611+
// Empty map: nothing to validate, should pass
14612+
obj := Outer{Data: map[string]Inner{}}
14613+
errs := validate.Struct(obj)
14614+
Equal(t, errs, nil)
14615+
}
14616+
14617+
func TestMapEmptyPointerStructValueNoError(t *testing.T) {
14618+
// Tests that empty pointer struct values in maps do not trigger validation errors
14619+
type Inner struct{}
14620+
14621+
type Outer struct {
14622+
Data map[string]*Inner `validate:"dive,keys,max=3,endkeys"`
14623+
}
14624+
validate := New()
14625+
14626+
// Key OK, pointer is non-nil empty struct: should pass
14627+
obj1 := Outer{Data: map[string]*Inner{"ok": {}}}
14628+
errs := validate.Struct(obj1)
14629+
Equal(t, errs, nil)
14630+
14631+
// Key OK, pointer is nil: no tags on Inner, so should pass
14632+
obj2 := Outer{Data: map[string]*Inner{"ok": nil}}
14633+
errs = validate.Struct(obj2)
14634+
Equal(t, errs, nil)
14635+
}

0 commit comments

Comments
 (0)