Schema-First Validation with VeeValidate 4 and Vue 3
Move VeeValidate 4 validation rules into one schema with useForm, then verify trigger timing and pin the major version.
08 Apr 2026, 04:51 UTC

One form, many places for rules
Adding a custom input to a Vue form often starts with a single v-model and a few inline rules. By the third field—especially with conditional inputs, password confirmation, or an async username check—the validation logic is scattered across the template, the <script setup> block, and sometimes a separate helper file. The useful takeaway is to move those rules into one validation schema and let VeeValidate 4’s useForm composable own the reactive form state. That keeps submission, errors, and field values in one place, but you still need to verify when validation runs and pin the package major version.
What useForm provides
In VeeValidate 4 on Vue 3, useForm({ validationSchema }) returns a form object with errors, values, isSubmitting, handleSubmit, resetForm, and setFieldValue. errors is keyed by field name, values holds the current field values, and isSubmitting is a boolean that flips while a submission promise is in flight. “Reactive” means the object updates automatically as the user types, so the template can read errors.email without manual event wiring. Use the built-in Field and ErrorMessage components for simple inputs, and useField when you need a custom control that still reports to the same schema.
Worked example: signup form
This example validates email, password, and confirm-password in a single schema. Adapt the import to whichever schema library your project already uses.
<script setup>
import { useForm, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';
const schema = yup.object({
email: yup.string().email('Enter a valid email').required('Email is required'),
password: yup.string().min(8, 'Password must be at least 8 characters').required('Password is required'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password'), null], 'Passwords must match')
.required('Confirm your password'),
});
const form = useForm({
validationSchema: schema,
});
async function onSubmit(values) {
// values.email, values.password, values.confirmPassword
const response = await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
if (!response.ok) {
// Handle network or API errors explicitly; isSubmitting alone does not cover them.
throw new Error('Signup failed');
}
}
</script>
<template>
<form @submit='form.handleSubmit(onSubmit)'>
<Field name='email' type='email' placeholder='Email' />
<ErrorMessage name='email' />
<Field name='password' type='password' placeholder='Password' />
<ErrorMessage name='password' />
<Field name='confirmPassword' type='password' placeholder='Confirm password' />
<ErrorMessage name='confirmPassword' />
<button type='submit' :disabled='form.isSubmitting'>Create account</button>
</form>
</template>
Run npm install vee-validate@4 yup in your project root. No special permissions are required. After installation, check that package.json lists vee-validate under a 4.x range. The risk is that a future major version can change composable signatures, so run your build and tests after upgrading.
Verify validation timing before shipping
Open the form and exercise each interaction:
- Tab into the email field, leave it empty, and blur out. Confirm whether the error appears immediately or only on change, depending on your configured interaction mode.
- Type an invalid email address and confirm the error updates without a submit attempt.
- Submit valid values and confirm
form.isSubmittingbecomestruewhile the request is in flight. - Call
form.resetForm()after success and confirm the fields and errors clear.
A practical check is to inspect form.errors.value in the browser console or Vue DevTools after each step. If validation runs at unexpected times, review VeeValidate’s validate-on-blur and validate-on-change settings for your Field components.
Trade-offs and limitations
- Schema-first validation can feel heavier for a tiny form with one or two fields.
- Interdependent dynamic rules are sometimes more verbose in a schema than as inline functions.
isSubmittingonly tracks the submit promise; failed network responses still need explicit error handling insideonSubmit.- Async validation rules require their own pending and error states; do not assume the form object covers them automatically.
- Schema libraries bring their own version constraints, so avoid duplicating field definitions between the schema and the UI.
When to adopt this pattern
Use useForm with a shared validation schema when a form has many fields, reused rules, or programmatic submission. For a simple contact form, plain Field with inline rules may be simpler. Pin vee-validate@^4.x in package.json and verify blur, focus, keyboard, and screen-reader flows before releasing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.