Skip to content

useValidation

useValidation creates or joins a validation scope. A scope validates its active schema/model registrations together and exposes structured issues and resolved error strings.

Call it during Vue component setup. No plugin is required unless you want application-wide message resolution or issue normalisation.

For run results, transformed output, snapshots, disposal and overlapping async runs, see Validation lifecycle.

Signatures

Create or join an orchestration-only scope:

ts
function useValidation(options?: ValidationScopeOptions): ValidationGroup

Register a Standard Schema and model:

ts
function useValidation<Schema extends StandardSchemaV1>(
  schema: MaybeRef<Schema>,
  model: ValidationData<Schema>,
  options?: ValidationOptions,
): ValidationController<Schema>

The model can be a reactive object, a ref containing the schema input, or an object whose fields are individual refs:

ts
const { errorsFor, validate } = useValidation(schema, { email, password })

The first call in a component tree creates a scope. Later calls in that component or its descendants join the nearest scope. Calling validate() on any group or controller validates every active registration in that scope.

Use { scope: 'new' } when a nested form must validate independently:

ts
const { errorsFor, validate } = useValidation(schema, model, { scope: 'new' })

Members at a glance

Every ValidationGroup and ValidationController has these scope members:

MemberTypeMeaning
issuesComputedRef<readonly ValidationIssue[]>All committed issues in the scope.
errorsComputedRef<readonly string[]>All committed issues resolved to display-ready strings.
isValidatingComputedRef<boolean>Whether any full, targeted or queued validation work is pending.
issuesFor(path)readonly ValidationIssue[]Structured issues at one exact path.
hasError(path)booleanWhether that exact path has at least one issue.
errorsFor(path)readonly string[]Resolved error strings at one exact path.
errorFor(path)string | undefinedThe first resolved error at one exact path.
validateFor(path)Promise<ValidationResult>Run complete matching schemas and publish fresh issues only at one exact path.
validate()Promise<ValidationResult>Validate every active registration in the scope.

Only the schema/model overload returns registration-local state:

MemberTypeMeaning
ownIssuesComputedRef<readonly ValidationIssue[]>Issues produced by this registration only.
resultReadonly<ShallowRef<RegistrationResult<Output>>>This registration's idle, valid or invalid state, including typed output on success.

Simple code can destructure the members it needs:

ts
const { errorsFor, hasError, validate, validateFor } = useValidation(schema, model)

Destructure result when you need registration-local state. The result unions and output access pattern are documented under results and transformed output.

Options

Both overloads accept scope options:

OptionTypePurpose
scope'new'Create an independent scope instead of joining the nearest one.
messagesMessageResolverResolve error strings for this scope or registration.
messagePrefixstringSupply a form-specific catalogue prefix to message resolvers.
describeIssueIssueNormaliserConvert vendor-specific issue data to a semantic identifier and values.

An argumentless call that joins an existing scope cannot change its policy. Configure messages, messagePrefix or describeIssue on the scope creator, on a schema registration, or on a new independent scope.

Schema registrations also accept:

OptionTypePurpose
atreadonly PropertyKey[]Prefix issue paths without changing the value passed to the schema.
ts
const { errorsFor, hasError, validateFor } = useValidation(addressSchema, address, {
  at: ['shipping'],
})

An issue at the schema-local path ['postcode'] now has the scope path ['shipping', 'postcode'].

Paths and selectors

Selectors match one exact path. Selecting a parent does not include descendant issues:

ts
issuesFor('address') // ['address'] only
issuesFor(['address', 'postcode']) // the nested field only

Use an array for nested paths; dotted strings are treated as one property key.

validateFor(path) uses the same path rules as the selectors. It captures the complete model and runs each complete matching Standard Schema, so cross-field rules still execute. It then publishes only issues whose resolved path exactly matches the selected path:

vue
<input
  v-model="email"
  :aria-invalid="hasError('email')"
  @blur="validateFor('email')"
>

Its returned ValidationResult describes only the selected path. A successful targeted result does not mean that the complete form is valid and must not be used to authorise submission. Targeted validation updates issue selectors but does not update registration result or transformed output; full validate() owns those submission states.

On a controller with at, selectors are relative to that prefix:

ts
hasError('postcode')
errorsFor('postcode')
validateFor('postcode')
// Matches the scope path ['shipping', 'postcode'].

An orchestration-only group has no prefix, so its selectors use complete scope paths. A pathless issue is selected with []; on a prefixed controller, [] selects the registration prefix itself.

ValidationIssue

FieldMeaning
rawOriginal Standard Schema issue, retained by identity.
vendorThe schema's Standard Schema vendor.
messageOriginal schema message.
localPathPath emitted by this registration's schema.
pathat plus localPath, used by the scope.
semanticOptional locale-independent identifier, interpolation values and plural count.

Errors are resolved lazily from issues. A locale change can therefore update errors, errorsFor() and errorFor() without another validation run. A value captured once in script is only a snapshot; retain it reactively when needed:

ts
const emailErrors = computed(() => errorsFor('email'))

See Message resolution for resolver precedence and adapters.

Continue to Validation lifecycle before consuming transformed output or coordinating async validation. For a complete descendant composition example, see Scopes and registrations.