Skip to content

Commit d57102d

Browse files
committed
fix: resolve panic when using cross-field validators with ValidateMap
When using validators like required_if, required_unless, excluded_if, or excluded_unless with ValidateMap(), the validator panicked with "Invalid field namespace" because getStructFieldOKInternal couldn't navigate primitive types to find referenced fields. This change makes getStructFieldOKInternal return found=false instead of panicking when it encounters types it can't navigate. This allows cross-field validators to handle the "not found" case gracefully using their defaultNotFoundValue parameter. Fixes #893
1 parent 79fba72 commit d57102d

File tree

2 files changed

+80
-2
lines changed

2 files changed

+80
-2
lines changed

util.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ BEGIN:
214214
}
215215

216216
// if got here there was more namespace, cannot go any deeper
217-
panic("Invalid field namespace")
217+
// return found=false instead of panicking to handle cases like ValidateMap
218+
// where cross-field validators (required_if, etc.) can't navigate non-struct parents
219+
return
218220
}
219221

220222
// asInt returns the parameter as an int64

validator_test.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2183,7 +2183,11 @@ func TestCrossNamespaceFieldValidation(t *testing.T) {
21832183
Equal(t, current.String(), "<*validator.SliceStruct Value>")
21842184
Equal(t, current.IsNil(), true)
21852185

2186-
PanicMatches(t, func() { v.getStructFieldOKInternal(reflect.ValueOf(1), "crazyinput") }, "Invalid field namespace")
2186+
// Test that invalid namespace on primitive type returns found=false instead of panicking
2187+
// This enables cross-field validators like required_if to work with ValidateMap
2188+
current, kind, _, ok = v.getStructFieldOKInternal(reflect.ValueOf(1), "crazyinput")
2189+
Equal(t, ok, false)
2190+
Equal(t, kind, reflect.Int)
21872191
}
21882192

21892193
func TestExistsValidation(t *testing.T) {
@@ -13905,6 +13909,78 @@ func TestValidate_ValidateMapCtxWithKeys(t *testing.T) {
1390513909
}
1390613910
}
1390713911

13912+
// TestValidateMapWithCrossFieldValidators tests that cross-field validators
13913+
// like required_if, required_unless, etc. don't panic when used with ValidateMap.
13914+
// This is a regression test for issue #893.
13915+
//
13916+
// Note: With ValidateMap, cross-field lookups return "not found" since there's no
13917+
// struct context. Validators handle this by using their defaultNotFoundValue:
13918+
// - required_if: condition not met (returns false) → field not required
13919+
// - required_unless: condition not met (returns false) → field required
13920+
// - excluded_if: condition not met (returns false) → field not excluded
13921+
// - excluded_unless: condition not met (returns false) → field must be excluded
13922+
func TestValidateMapWithCrossFieldValidators(t *testing.T) {
13923+
validate := New()
13924+
13925+
// Test required_if - should not panic
13926+
// Cross-field lookup returns not found → condition not met → field not required
13927+
data := map[string]interface{}{
13928+
"name": "hello",
13929+
"id": 123,
13930+
}
13931+
rules := map[string]interface{}{
13932+
"name": "required_if=id 345",
13933+
"id": "required",
13934+
}
13935+
errs := validate.ValidateMap(data, rules)
13936+
Equal(t, len(errs), 0)
13937+
13938+
// Test required_unless - should not panic
13939+
// Cross-field lookup returns not found → condition not met → field required
13940+
// Since name has a value, validation passes
13941+
rules2 := map[string]interface{}{
13942+
"name": "required_unless=id 345",
13943+
"id": "required",
13944+
}
13945+
errs = validate.ValidateMap(data, rules2)
13946+
Equal(t, len(errs), 0)
13947+
13948+
// Test excluded_if - should not panic
13949+
// Cross-field lookup returns not found → condition not met → field not excluded
13950+
rules3 := map[string]interface{}{
13951+
"name": "excluded_if=id 123",
13952+
"id": "required",
13953+
}
13954+
errs = validate.ValidateMap(data, rules3)
13955+
Equal(t, len(errs), 0)
13956+
13957+
// Test excluded_unless - should not panic
13958+
// Cross-field lookup returns not found → condition not met → field must be excluded
13959+
// Since name has a value, validation FAILS (this is expected behavior)
13960+
rules4 := map[string]interface{}{
13961+
"name": "excluded_unless=id 123",
13962+
"id": "required",
13963+
}
13964+
errs = validate.ValidateMap(data, rules4)
13965+
Equal(t, len(errs), 1) // Fails because name has value but condition can't be verified
13966+
13967+
// Test excluded_unless with empty value - should pass since field is excluded
13968+
dataEmpty := map[string]interface{}{
13969+
"name": "",
13970+
"id": 123,
13971+
}
13972+
errs = validate.ValidateMap(dataEmpty, rules4)
13973+
Equal(t, len(errs), 0)
13974+
13975+
// Test with empty name - required_if condition not met, so empty is ok
13976+
data2 := map[string]interface{}{
13977+
"name": "",
13978+
"id": 123,
13979+
}
13980+
errs = validate.ValidateMap(data2, rules)
13981+
Equal(t, len(errs), 0)
13982+
}
13983+
1390813984
func TestValidate_VarWithKey(t *testing.T) {
1390913985
validate := New()
1391013986
errs := validate.VarWithKey("email", "invalidemail", "required,email")

0 commit comments

Comments
 (0)