Skip to content

Commit e9b900c

Browse files
authored
fix: resolve panic when using cross-field validators with ValidateMap (#1508)
1 parent 7aba81c commit e9b900c

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
@@ -2245,7 +2245,11 @@ func TestCrossNamespaceFieldValidation(t *testing.T) {
22452245
Equal(t, current.String(), "<*validator.SliceStruct Value>")
22462246
Equal(t, current.IsNil(), true)
22472247

2248-
PanicMatches(t, func() { v.getStructFieldOKInternal(reflect.ValueOf(1), "crazyinput") }, "Invalid field namespace")
2248+
// Test that invalid namespace on primitive type returns found=false instead of panicking
2249+
// This enables cross-field validators like required_if to work with ValidateMap
2250+
_, kind, _, ok = v.getStructFieldOKInternal(reflect.ValueOf(1), "crazyinput")
2251+
Equal(t, ok, false)
2252+
Equal(t, kind, reflect.Int)
22492253
}
22502254

22512255
func TestExistsValidation(t *testing.T) {
@@ -13967,6 +13971,78 @@ func TestValidate_ValidateMapCtxWithKeys(t *testing.T) {
1396713971
}
1396813972
}
1396913973

13974+
// TestValidateMapWithCrossFieldValidators tests that cross-field validators
13975+
// like required_if, required_unless, etc. don't panic when used with ValidateMap.
13976+
// This is a regression test for issue #893.
13977+
//
13978+
// Note: With ValidateMap, cross-field lookups return "not found" since there's no
13979+
// struct context. Validators handle this by using their defaultNotFoundValue:
13980+
// - required_if: condition not met (returns false) → field not required
13981+
// - required_unless: condition not met (returns false) → field required
13982+
// - excluded_if: condition not met (returns false) → field not excluded
13983+
// - excluded_unless: condition not met (returns false) → field must be excluded
13984+
func TestValidateMapWithCrossFieldValidators(t *testing.T) {
13985+
validate := New()
13986+
13987+
// Test required_if - should not panic
13988+
// Cross-field lookup returns not found → condition not met → field not required
13989+
data := map[string]interface{}{
13990+
"name": "hello",
13991+
"id": 123,
13992+
}
13993+
rules := map[string]interface{}{
13994+
"name": "required_if=id 345",
13995+
"id": "required",
13996+
}
13997+
errs := validate.ValidateMap(data, rules)
13998+
Equal(t, len(errs), 0)
13999+
14000+
// Test required_unless - should not panic
14001+
// Cross-field lookup returns not found → condition not met → field required
14002+
// Since name has a value, validation passes
14003+
rules2 := map[string]interface{}{
14004+
"name": "required_unless=id 345",
14005+
"id": "required",
14006+
}
14007+
errs = validate.ValidateMap(data, rules2)
14008+
Equal(t, len(errs), 0)
14009+
14010+
// Test excluded_if - should not panic
14011+
// Cross-field lookup returns not found → condition not met → field not excluded
14012+
rules3 := map[string]interface{}{
14013+
"name": "excluded_if=id 123",
14014+
"id": "required",
14015+
}
14016+
errs = validate.ValidateMap(data, rules3)
14017+
Equal(t, len(errs), 0)
14018+
14019+
// Test excluded_unless - should not panic
14020+
// Cross-field lookup returns not found → condition not met → field must be excluded
14021+
// Since name has a value, validation FAILS (this is expected behavior)
14022+
rules4 := map[string]interface{}{
14023+
"name": "excluded_unless=id 123",
14024+
"id": "required",
14025+
}
14026+
errs = validate.ValidateMap(data, rules4)
14027+
Equal(t, len(errs), 1) // Fails because name has value but condition can't be verified
14028+
14029+
// Test excluded_unless with empty value - should pass since field is excluded
14030+
dataEmpty := map[string]interface{}{
14031+
"name": "",
14032+
"id": 123,
14033+
}
14034+
errs = validate.ValidateMap(dataEmpty, rules4)
14035+
Equal(t, len(errs), 0)
14036+
14037+
// Test with empty name - required_if condition not met, so empty is ok
14038+
data2 := map[string]interface{}{
14039+
"name": "",
14040+
"id": 123,
14041+
}
14042+
errs = validate.ValidateMap(data2, rules)
14043+
Equal(t, len(errs), 0)
14044+
}
14045+
1397014046
func TestValidate_VarWithKey(t *testing.T) {
1397114047
validate := New()
1397214048
errs := validate.VarWithKey("email", "invalidemail", "required,email")

0 commit comments

Comments
 (0)