Skip to content

Conversation

@shindonghwi
Copy link

@shindonghwi shindonghwi commented Dec 18, 2025

Fixes #766

Using aliases with OR operator causes panic:

validate.RegisterAlias("bid2", "numeric")
// validate:"numeric|bid2" → panic: Undefined validation function 'bid2'

The issue is that alias lookup only happens for comma-separated tags, not OR-separated ones. Fixed by adding alias check in the OR parsing logic.

Added tests for various cases (built-in aliases, custom aliases, different positions, etc).

@shindonghwi shindonghwi requested a review from a team as a code owner December 18, 2025 00:11
@shindonghwi shindonghwi force-pushed the fix/alias-with-or-operator-766 branch from b792454 to df02f70 Compare December 18, 2025 00:12
@coveralls
Copy link

coveralls commented Dec 18, 2025

Coverage Status

coverage: 73.773% (+0.02%) from 73.749%
when pulling c3989b5 on shindonghwi:fix/alias-with-or-operator-766
into b0e4ba2 on go-playground:master.

@shindonghwi shindonghwi force-pushed the fix/alias-with-or-operator-766 branch from df02f70 to b5ae5d3 Compare December 18, 2025 00:15
zemzale
zemzale previously approved these changes Dec 18, 2025
Copy link
Member

@zemzale zemzale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks like a good fix, just some style issues from me.

Comment on lines 292 to 309
} else if aliasTag, isAlias := v.aliases[current.tag]; isAlias {
// Handle alias within OR expression (issue #766)
// Recursively parse the alias tag
aliasFirst, aliasLast := v.parseFieldTagsRecursive(aliasTag, fieldName, current.tag, true)

// Copy the first parsed alias tag's validation data to current
current.tag = aliasFirst.tag
current.fn = aliasFirst.fn
current.runValidationWhenNil = aliasFirst.runValidationWhenNil
current.hasParam = aliasFirst.hasParam
current.param = aliasFirst.param
current.typeof = aliasFirst.typeof
current.hasAlias = true
// Note: isBlockEnd is NOT copied here; it will be set at the end of the OR loop

// If alias expanded to multiple tags, insert them into the chain
if aliasFirst.next != nil {
// Save what comes after current in the OR chain
nextInChain := current.next
// Link the rest of the alias chain
current.next = aliasFirst.next
// Connect the last alias tag to what was after current
aliasLast.next = nextInChain
// Clear isBlockEnd since this may not be the last tag in the OR expression
aliasLast.isBlockEnd = false
// Update current to point to the last tag in the alias chain
current = aliasLast
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments are a bit excessive.

Suggested change
} else if aliasTag, isAlias := v.aliases[current.tag]; isAlias {
// Handle alias within OR expression (issue #766)
// Recursively parse the alias tag
aliasFirst, aliasLast := v.parseFieldTagsRecursive(aliasTag, fieldName, current.tag, true)
// Copy the first parsed alias tag's validation data to current
current.tag = aliasFirst.tag
current.fn = aliasFirst.fn
current.runValidationWhenNil = aliasFirst.runValidationWhenNil
current.hasParam = aliasFirst.hasParam
current.param = aliasFirst.param
current.typeof = aliasFirst.typeof
current.hasAlias = true
// Note: isBlockEnd is NOT copied here; it will be set at the end of the OR loop
// If alias expanded to multiple tags, insert them into the chain
if aliasFirst.next != nil {
// Save what comes after current in the OR chain
nextInChain := current.next
// Link the rest of the alias chain
current.next = aliasFirst.next
// Connect the last alias tag to what was after current
aliasLast.next = nextInChain
// Clear isBlockEnd since this may not be the last tag in the OR expression
aliasLast.isBlockEnd = false
// Update current to point to the last tag in the alias chain
current = aliasLast
}
} else if aliasTag, isAlias := v.aliases[current.tag]; isAlias {
// Handle alias within OR expression (issue #766)
// Recursively parse the alias tag
aliasFirst, aliasLast := v.parseFieldTagsRecursive(aliasTag, fieldName, current.tag, true)
// Note: isBlockEnd is NOT copied here; it will be set at the end of the OR loop
current.tag = aliasFirst.tag
current.fn = aliasFirst.fn
current.runValidationWhenNil = aliasFirst.runValidationWhenNil
current.hasParam = aliasFirst.hasParam
current.param = aliasFirst.param
current.typeof = aliasFirst.typeof
current.hasAlias = true
// If alias expanded to multiple tags, insert them into the chain
if aliasFirst.next != nil {
nextInChain := current.next
current.next = aliasFirst.next
aliasLast.next = nextInChain
aliasLast.isBlockEnd = false
current = aliasLast
}

Comment on lines 699 to 796
// TestAliasWithOrOperator tests using registered aliases within OR expressions (issue #766)
func TestAliasWithOrOperator(t *testing.T) {
validate := New()

// Test 1: Built-in alias "iscolor" with OR - alias at end
type Test1 struct {
Field string `validate:"numeric|iscolor"`
}

t1 := Test1{Field: "#fff"}
errs := validate.Struct(t1)
Equal(t, errs, nil) // Should pass - valid hex color

t1.Field = "123"
errs = validate.Struct(t1)
Equal(t, errs, nil) // Should pass - numeric

t1.Field = "invalid!"
errs = validate.Struct(t1)
NotEqual(t, errs, nil) // Should fail - neither numeric nor color

// Test 2: Built-in alias with OR - alias at start
type Test2 struct {
Field string `validate:"iscolor|numeric"`
}

t2 := Test2{Field: "456"}
errs = validate.Struct(t2)
Equal(t, errs, nil) // Should pass - numeric

t2.Field = "rgb(255,0,0)"
errs = validate.Struct(t2)
Equal(t, errs, nil) // Should pass - valid rgb color

// Test 3: Custom alias with OR
validate.RegisterAlias("customnum", "numeric")

type Test3 struct {
Field string `validate:"alpha|customnum"`
}

t3 := Test3{Field: "789"}
errs = validate.Struct(t3)
Equal(t, errs, nil) // Should pass - numeric via alias

t3.Field = "abc"
errs = validate.Struct(t3)
Equal(t, errs, nil) // Should pass - alpha

// Test 4: Multiple aliases in OR
validate.RegisterAlias("customalpha", "alpha")

type Test4 struct {
Field string `validate:"customnum|customalpha"`
}

t4 := Test4{Field: "xyz"}
errs = validate.Struct(t4)
Equal(t, errs, nil) // Should pass - alpha via alias

// Test 5: Three-way OR with alias in middle
type Test5 struct {
Field string `validate:"alpha|customnum|email"`
}

t5 := Test5{Field: "test@example.com"}
errs = validate.Struct(t5)
Equal(t, errs, nil) // Should pass - email

// Test 6: Alias with parameter
validate.RegisterAlias("customgt5", "gt=5")

type Test6 struct {
Field int `validate:"eq=0|customgt5"`
}

t6 := Test6{Field: 0}
errs = validate.Struct(t6)
Equal(t, errs, nil) // Should pass - eq=0

t6.Field = 10
errs = validate.Struct(t6)
Equal(t, errs, nil) // Should pass - gt=5 via alias

t6.Field = 3
errs = validate.Struct(t6)
NotEqual(t, errs, nil) // Should fail - not 0 and not > 5

// Test 7: Another built-in alias with OR (country_code)
type Test7 struct {
Field string `validate:"numeric|country_code"`
}

t7 := Test7{Field: "US"}
errs = validate.Struct(t7)
Equal(t, errs, nil) // Should pass - valid country code
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again the comments are a bit too much. None of them are needed, or should just be used as names for t.Run subtests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Removed all comments and converted tests to use t.Run subtests. @zemzale

Alias lookup was missing in OR parsing logic, causing panic when using
aliases like `numeric|myalias`. Fixed by adding alias check before the
undefined validation panic.

Fixes go-playground#766
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aliases or'ed with other functions causes a panic Undefined validation function

3 participants