fix: allow combining global and local validation rules (#5025)#5146
fix: allow combining global and local validation rules (#5025)#5146logaretm wants to merge 1 commit into
Conversation
When both global rules (via defineRule) and local/inline validator
functions are used on the same field, they can now be combined using
either the object syntax or the array syntax.
Object syntax: { required: true, myLocalRule: myValidatorFn }
Array syntax: ['required', myValidatorFn]
Previously, the object syntax would throw "No such validator" because
function values were treated as rule parameters rather than inline
validators. The array syntax would throw "rule is not a function"
because string entries were called directly instead of being resolved
as global rules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 0857677 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for vee-validate-docs canceled.
|
✅ Deploy Preview for vee-validate-v5 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
This PR fixes #5025 by enabling mixed usage of global rule references (string/object rule names) and inline/local validator functions in the same rules expression, across both object and array syntaxes.
Changes:
- Updated
_validateto resolve string entries inside rule arrays as global rules, and to support object rules that mix global rules with inline function validators. - Updated
useFieldrule normalization to avoid corrupting mixed object rules that contain function values. - Added regression tests covering mixed global+local rules in both object and array syntaxes, plus a changeset for a patch release.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/vee-validate/src/validate.ts | Adds support for resolving string rules in arrays and splitting object rule entries into global vs inline validators. |
| packages/vee-validate/src/useField.ts | Adjusts computed validator normalization to pass through rule objects containing function values. |
| packages/vee-validate/tests/validate.spec.ts | Adds tests for combining global and local rules in both object and array syntaxes. |
| .changeset/fix-5025-global-local-rules.md | Patch changeset entry documenting the fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (isCallable(rules[key])) { | ||
| inlineFns.push(rules[key] as GenericValidateFunction<TInput>); | ||
| } else { | ||
| globalRulesObj[key] = rules[key]; |
There was a problem hiding this comment.
The new object-rules branch treats any function-valued entry as an inline validator. This changes behavior for cases where the key refers to a defined global rule but the rule’s params are intentionally a function (e.g. passing a locator/function param). In that case the global rule will no longer run. Consider disambiguating by checking whether resolveRule(key) exists (or whether the function is a locator) before classifying it as an inline validator; only treat it as inline when there is no global rule with that name.
| if (isCallable(rules[key])) { | |
| inlineFns.push(rules[key] as GenericValidateFunction<TInput>); | |
| } else { | |
| globalRulesObj[key] = rules[key]; | |
| const value = rules[key]; | |
| const globalRule = resolveRule(key); | |
| // Treat as inline validator only when there is no global rule with this name | |
| if (isCallable(value) && !globalRule) { | |
| inlineFns.push(value as GenericValidateFunction<TInput>); | |
| } else { | |
| globalRulesObj[key] = value; |
| for (let j = 0; j < ruleNames.length; j++) { | ||
| const normalizedContext = { | ||
| ...field, | ||
| rules: stringRules, | ||
| }; |
There was a problem hiding this comment.
In the array validation pipeline, normalizedContext is recreated inside the inner ruleNames loop even though it doesn’t change. This adds avoidable allocations/spreads for every rule and every validation run. Move normalizedContext creation outside the inner loop (once per string entry) and reuse it when calling _test.
| for (let j = 0; j < ruleNames.length; j++) { | |
| const normalizedContext = { | |
| ...field, | |
| rules: stringRules, | |
| }; | |
| const normalizedContext = { | |
| ...field, | |
| rules: stringRules, | |
| }; | |
| for (let j = 0; j < ruleNames.length; j++) { |
| // If the rules object contains function values (inline validators mixed with global rules), | ||
| // pass it through as-is so that _validate can handle both types (#5025) | ||
| if (rulesValue && typeof rulesValue === 'object') { | ||
| const hasInlineFns = Object.values(rulesValue).some(v => isCallable(v)); | ||
| if (hasInlineFns) { | ||
| return rulesValue; | ||
| } | ||
| } |
There was a problem hiding this comment.
This pass-through logic flags any function-valued rule entry as an inline validator. That can break object rule usage where a function is intended to be a parameter for a globally defined rule (or a locator), because it will bypass normalizeRules and later be treated as an inline validator. Consider only treating function values as inline validators when the corresponding key does not resolve to a global rule name (and/or when the function isn’t a locator).
| // Local rule fails (bails: false to see both errors) | ||
| result = await validate('ab', { required: true, myLocalRule } as any, { bails: false }); | ||
| expect(result.valid).toBe(false); | ||
| expect(result.errors).toContain('Must be at least 3 characters'); |
There was a problem hiding this comment.
The comment says “bails: false to see both errors”, but this test case only exercises a value where one rule fails. Add an assertion/case where both the global and local rules fail with bails: false (e.g. empty string) and verify both error messages are returned, to ensure the combined-rules behavior works in the non-bailing path.
| // Local rule fails | ||
| result = await validate('ab', ['required', myLocalRule] as any, { bails: false }); | ||
| expect(result.valid).toBe(false); | ||
| expect(result.errors).toContain('Must be at least 3 characters'); |
There was a problem hiding this comment.
Similar to the object-syntax test: the bails: false case here only covers a value where the local rule fails. Add a case where both the global string rule and the local function rule fail with bails: false (e.g. empty string) and assert that both errors are collected.
| expect(result.errors).toContain('Must be at least 3 characters'); | |
| expect(result.errors).toContain('Must be at least 3 characters'); | |
| // Both global and local rules fail with bails: false, both errors should be collected | |
| result = await validate('', ['required', myLocalRule] as any, { bails: false }); | |
| expect(result.valid).toBe(false); | |
| expect(result.errors).toContain('This field is required'); | |
| expect(result.errors).toContain('Must be at least 3 characters'); |
Summary
Fixes #5025
{ required: true, myLocalRule: myValidatorFn }now works by separating function-valued entries (inline validators) from string-keyed global rule references, running both['required', myValidatorFn]now works by resolving string elements as global rules instead of calling them directly as functionsChanges
packages/vee-validate/src/validate.ts_validate, string entries are now resolved vianormalizeRulesand_test(global rule lookup) instead of being called as functions_testand inline functions directlypackages/vee-validate/src/useField.ts_validateinstead of going throughnormalizeRuleswhich would corrupt the function referencesTest plan
🤖 Generated with Claude Code