フォームの入力チェックを、自前で書くと、項目が増えるたびにコードが膨らみます。vee-validateは、Vue向けのフォームのバリデーションライブラリで、値、エラー、検証のタイミングをまとめて管理できます。この記事では、Vue 3(<script setup>)を前提に、導入から基本の使い方、独自のルール、日本語化までを整理します。自前で検証する場合の考え方は、入力フォームのバリデーション機能を実装する方法をご覧ください。
導入
vee-validate本体に加え、スキーマで検証ルールを書くために、Yup(または Zod)と、そのアダプターを入れます。アダプターの@vee-validate/yupを入れ忘れやすいので注意してください。
# vee-validate 本体、Yup との連携用アダプター、Yup npm i vee-validate @vee-validate/yup yup
2つの使い方
公式は、vee-validateの使い方を2つ案内しています。混ぜて使うこともできます。
- Composition API:
useForm、defineFieldを使います。既存のUIの部品と組み合わせやすい方法です。 - コンポーネント:
<Form>、<Field>、<ErrorMessage>を、テンプレートの中で使います。
基本:useForm と defineField
まず、書いてしまいがちな、動かない例です。
<script setup>
import { useForm } from 'vee-validate'
const { handleSubmit, errors, values } = useForm({ validationSchema: schema })
// NG①:values.name は、ref ではなく、ただの値(最初は undefined)。
// v-model="name" は、const にバインドすることになり、動かない
const name = values.name
// NG②:handleSubmit(onSubmit) は、送信ハンドラとなる関数を「返す」だけで、呼ばない
// <form @submit="handleSubmit(onSubmit)">
</script>
values.nameは、refではなく、ただの値です。v-modelをconstにバインドしても、動きません。入力欄との結びつけには、defineField('name')を使います。handleSubmit(onSubmit)は、検証に通ったときにコールバックを呼ぶ送信ハンドラとなる関数を返すだけです。@submit="handleSubmit(onSubmit)"と書いても、返された関数が呼ばれず、何も起きません。const onSubmit = handleSubmit(...)として受け取り、@submitに渡します。
正しい書き方です。スキーマは、@vee-validate/yupのtoTypedSchema()で包んで渡します。公式の入門ページがこの書き方で、フォームの値に型が付き、必須項目の値がnull許容にならないためです。defineFieldは、[値のモデル, 属性]を返します。
<script setup>
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/yup'
import * as yup from 'yup'
// toTypedSchema で包んで渡す(型が付き、必須項目の値が null 許容にならない)
// label は、エラーメッセージの ${path} に使われる項目名
const schema = toTypedSchema(
yup.object({
name: yup.string().label('名前').required(),
email: yup.string().label('メールアドレス').required().email()
})
)
const { handleSubmit, errors, defineField } = useForm({
validationSchema: schema
})
// defineField は [値のモデル, 属性] を返す
const [name, nameAttrs] = defineField('name')
const [email, emailAttrs] = defineField('email')
// handleSubmit は、検証に通ったときだけコールバックを呼ぶ、送信ハンドラを返す
const onSubmit = handleSubmit((values) => {
console.log('送信データ:', values)
})
</script>
<template>
<!-- novalidate:ブラウザ標準の検証を止め、エラー表示を vee-validate に統一する -->
<form novalidate @submit.prevent="onSubmit">
<div>
<label for="name">名前</label>
<input
id="name"
v-model="name"
v-bind="nameAttrs"
type="text"
autocomplete="name"
:aria-invalid="!!errors.name"
aria-describedby="name-error"
>
<p id="name-error" role="alert">{{ errors.name }}</p>
</div>
<div>
<label for="email">メールアドレス</label>
<input
id="email"
v-model="email"
v-bind="emailAttrs"
type="email"
autocomplete="email"
:aria-invalid="!!errors.email"
aria-describedby="email-error"
>
<p id="email-error" role="alert">{{ errors.email }}</p>
</div>
<!-- 送信ボタンは無効にしない。押したときに、エラーを示す -->
<button type="submit">送信</button>
</form>
</template>
送信ボタンは、エラーがある間、無効にしない設計にしています。押せない理由が分からなくなるためです。ラベル、aria-invalid、aria-describedby、role="alert"の意味は、入力フォームのバリデーション機能を実装する方法で解説しています。
コンポーネントで書く:Form・Field・ErrorMessage
テンプレートの中で完結させたい場合は、コンポーネントが便利です。<Form>に:validation-schemaを渡し、各<Field>にnameを付け、対応する<ErrorMessage>を置きます。検証に通ったときだけ、@submitのハンドラに値が渡されます。
<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/yup'
import * as yup from 'yup'
const schema = toTypedSchema(
yup.object({
email: yup.string().label('メールアドレス').required().email(),
password: yup.string().label('パスワード').required().min(8)
})
)
// 検証に通ったときだけ、値が渡される
function onSubmit(values) {
console.log('送信データ:', values)
}
</script>
<template>
<Form :validation-schema="schema" @submit="onSubmit">
<label for="email">メールアドレス</label>
<Field id="email" name="email" type="email" />
<ErrorMessage name="email" />
<label for="password">パスワード</label>
<Field id="password" name="password" type="password" />
<ErrorMessage name="password" />
<button type="submit">送信</button>
</Form>
</template>
Yupの代わりに、Zodも使えます。
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
// Yup の代わりに Zod も使える(npm i @vee-validate/zod zod)
const schema = toTypedSchema(
z.object({
email: z.string().min(1, '必須です').email('正しいメール形式で入力してください')
})
)
独自のルールを書く
Yupの組み込みのルール(required、min、matches、email)で足りないときは、.test()で独自のルールを書きます。パスワードの確認は、oneOfにyup.refを渡す書き方が定番です。
import * as yup from 'yup'
const schema = yup.object({
password: yup
.string()
.label('パスワード')
.required()
.min(8)
.matches(/[A-Z]/, '${path}に大文字を含めてください'),
// パスワードの確認:password と同じ値でなければ、エラー
passwordConfirm: yup
.string()
.label('確認用パスワード')
.required()
.oneOf([yup.ref('password')], 'パスワードが一致しません'),
// 独自のルール:test(名前, メッセージ, 検証する関数)。真を返せば、OK
username: yup
.string()
.label('ユーザー名')
.test('no-spaces', '空白は使えません', (value) => !value || !/\s/.test(value))
})
なお、Yupのrequired()は、空文字も「未入力」として扱います。email()は、HTMLの仕様と同じ正規表現で判定しますが、公式は、すべての有効なアドレスを網羅できるわけではなく、確実に確かめる方法は確認メールの送信だけ、と注意しています。
エラーメッセージを日本語にする
Yupの既定のメッセージは英語です。項目ごとにメッセージを書く代わりに、setLocaleで、共通のメッセージをまとめて設定できます。スキーマを作る前に、1度だけ呼びます。項目名は、スキーマ側の.label()で指定すると、${path}に入ります。
// main.js など、アプリの起動時に、1度だけ設定する(スキーマを作る前に)
import { setLocale } from 'yup'
setLocale({
mixed: {
required: '${path}は必須です'
},
string: {
min: '${path}は${min}文字以上で入力してください',
email: '${path}の形式が正しくありません'
}
})
// 項目名は、スキーマ側で .label('名前') と指定すると、${path} に入る
関連記事
- 項目を増減できるフォーム(
useFieldArray)は動的フォームの作り方とバリデーション設計 - 検証のロジックを自作のComposableにするならComposable関数でロジックを共通化する方法
- フォームの入力値の扱いは双方向データバインディングでフォームの入力値を反映させる方法、v-modelの仕組みを完全理解
よくある質問(FAQ)
handleSubmit(コールバック)は、送信ハンドラとなる関数を返します。const onSubmit = handleSubmit(...)と受け取り、@submit="onSubmit"のように渡してください。テンプレートに@submit="handleSubmit(onSubmit)"と書いても、返された関数が呼ばれません。@vee-validate/yupのtoTypedSchema()で包む書き方を示しています。型が付き、必須の項目の値がnull許容にならないためです。setLocaleで、共通のメッセージを設定します。項目名は、スキーマの.label('名前')で指定すると、メッセージの${path}に入ります。まとめ
vee-validateは、useFormとdefineField、または<Form>と<Field>で、値、エラー、検証のタイミングを一元管理できます。YupのスキーマはtoTypedSchema()で包み、handleSubmitは返された関数を@submitに渡します。独自のルールは.test()、日本語化はsetLocaleで行い、送信ボタンは無効にしない設計にしてください。

