Skip to content

Commit df02f70

Browse files
committed
fix: resolve panic when using aliases with OR operator (#766)
When using RegisterAlias with OR operator (|), the validator panics with "Undefined validation function" error. This happens because aliases are only checked when parsing comma-separated tags, not when parsing OR- separated values. This fix adds alias lookup in the OR parsing logic. When an alias is found within an OR expression, it is recursively parsed and properly inserted into the validation chain. Fixes #766
1 parent 79fba72 commit df02f70

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

cache.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,34 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
289289
if wrapper, ok := v.validations[current.tag]; ok {
290290
current.fn = wrapper.fn
291291
current.runValidationWhenNil = wrapper.runValidationOnNil
292+
} else if aliasTag, isAlias := v.aliases[current.tag]; isAlias {
293+
// Handle alias within OR expression (issue #766)
294+
// Recursively parse the alias tag
295+
aliasFirst, aliasLast := v.parseFieldTagsRecursive(aliasTag, fieldName, current.tag, true)
296+
297+
// Copy the first parsed alias tag's validation data to current
298+
current.tag = aliasFirst.tag
299+
current.fn = aliasFirst.fn
300+
current.runValidationWhenNil = aliasFirst.runValidationWhenNil
301+
current.hasParam = aliasFirst.hasParam
302+
current.param = aliasFirst.param
303+
current.typeof = aliasFirst.typeof
304+
current.hasAlias = true
305+
// Note: isBlockEnd is NOT copied here; it will be set at the end of the OR loop
306+
307+
// If alias expanded to multiple tags, insert them into the chain
308+
if aliasFirst.next != nil {
309+
// Save what comes after current in the OR chain
310+
nextInChain := current.next
311+
// Link the rest of the alias chain
312+
current.next = aliasFirst.next
313+
// Connect the last alias tag to what was after current
314+
aliasLast.next = nextInChain
315+
// Clear isBlockEnd since this may not be the last tag in the OR expression
316+
aliasLast.isBlockEnd = false
317+
// Update current to point to the last tag in the alias chain
318+
current = aliasLast
319+
}
292320
} else {
293321
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
294322
}

validator_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,104 @@ func TestAliasTags(t *testing.T) {
696696
PanicMatches(t, func() { validate.RegisterAlias("exists!", "gt=5,lt=10") }, "Alias 'exists!' either contains restricted characters or is the same as a restricted tag needed for normal operation")
697697
}
698698

699+
// TestAliasWithOrOperator tests using registered aliases within OR expressions (issue #766)
700+
func TestAliasWithOrOperator(t *testing.T) {
701+
validate := New()
702+
703+
// Test 1: Built-in alias "iscolor" with OR - alias at end
704+
type Test1 struct {
705+
Field string `validate:"numeric|iscolor"`
706+
}
707+
708+
t1 := Test1{Field: "#fff"}
709+
errs := validate.Struct(t1)
710+
Equal(t, errs, nil) // Should pass - valid hex color
711+
712+
t1.Field = "123"
713+
errs = validate.Struct(t1)
714+
Equal(t, errs, nil) // Should pass - numeric
715+
716+
t1.Field = "invalid!"
717+
errs = validate.Struct(t1)
718+
NotEqual(t, errs, nil) // Should fail - neither numeric nor color
719+
720+
// Test 2: Built-in alias with OR - alias at start
721+
type Test2 struct {
722+
Field string `validate:"iscolor|numeric"`
723+
}
724+
725+
t2 := Test2{Field: "456"}
726+
errs = validate.Struct(t2)
727+
Equal(t, errs, nil) // Should pass - numeric
728+
729+
t2.Field = "rgb(255,0,0)"
730+
errs = validate.Struct(t2)
731+
Equal(t, errs, nil) // Should pass - valid rgb color
732+
733+
// Test 3: Custom alias with OR
734+
validate.RegisterAlias("customnum", "numeric")
735+
736+
type Test3 struct {
737+
Field string `validate:"alpha|customnum"`
738+
}
739+
740+
t3 := Test3{Field: "789"}
741+
errs = validate.Struct(t3)
742+
Equal(t, errs, nil) // Should pass - numeric via alias
743+
744+
t3.Field = "abc"
745+
errs = validate.Struct(t3)
746+
Equal(t, errs, nil) // Should pass - alpha
747+
748+
// Test 4: Multiple aliases in OR
749+
validate.RegisterAlias("customalpha", "alpha")
750+
751+
type Test4 struct {
752+
Field string `validate:"customnum|customalpha"`
753+
}
754+
755+
t4 := Test4{Field: "xyz"}
756+
errs = validate.Struct(t4)
757+
Equal(t, errs, nil) // Should pass - alpha via alias
758+
759+
// Test 5: Three-way OR with alias in middle
760+
type Test5 struct {
761+
Field string `validate:"alpha|customnum|email"`
762+
}
763+
764+
t5 := Test5{Field: "test@example.com"}
765+
errs = validate.Struct(t5)
766+
Equal(t, errs, nil) // Should pass - email
767+
768+
// Test 6: Alias with parameter
769+
validate.RegisterAlias("customgt5", "gt=5")
770+
771+
type Test6 struct {
772+
Field int `validate:"eq=0|customgt5"`
773+
}
774+
775+
t6 := Test6{Field: 0}
776+
errs = validate.Struct(t6)
777+
Equal(t, errs, nil) // Should pass - eq=0
778+
779+
t6.Field = 10
780+
errs = validate.Struct(t6)
781+
Equal(t, errs, nil) // Should pass - gt=5 via alias
782+
783+
t6.Field = 3
784+
errs = validate.Struct(t6)
785+
NotEqual(t, errs, nil) // Should fail - not 0 and not > 5
786+
787+
// Test 7: Another built-in alias with OR (country_code)
788+
type Test7 struct {
789+
Field string `validate:"numeric|country_code"`
790+
}
791+
792+
t7 := Test7{Field: "US"}
793+
errs = validate.Struct(t7)
794+
Equal(t, errs, nil) // Should pass - valid country code
795+
}
796+
699797
func TestNilValidator(t *testing.T) {
700798
type TestStruct struct {
701799
Test string `validate:"required"`

0 commit comments

Comments
 (0)