- 技術 💻
Yupとは・使い方|エラー原文とZod比較の実測つき
結論
Yup は、JavaScript・TypeScript で「入力値がこの形・この決まりに合っているか」をスキーマ(ルールの定義)で確かめるバリデーションライブラリです。値の変換(cast)と検証を一度に行い、フォームでは React Hook Form や Formik と組み合わせて使います。
この記事でわかること
- string・number・when・test・transform など主な書き方と実行結果
- ValidationError の中身、日本語のエラーメッセージ、InferType の型
- 同じスキーマで Zod・Valibot・Joi と比べたコード量・サイズ・速さの実測
Yup は「スキーマを作る → 値を渡す → 変換して検証する → 合格なら変換後の値、不合格なら ValidationError」という流れで動きます。
flowchart TD
A[入力値] --> B[cast<br>型に合わせて変換]
B --> C{テストを順に実行}
C -->|全部通る| D[変換後の値を返す]
C -->|どれか失敗| E[ValidationError]
E --> F[path・message・errors・inner]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| Yupとは | スキーマで値を変換・検証するライブラリ。MIT ライセンス | Yupとは |
| yup 使い方(最初の一歩) | npm i yup → yup.object({...}) → validate() |
インストールと最初のバリデーション |
| エラーを全部出したい | { abortEarly: false } を付けて inner を読む |
validateとvalidateSync |
| 条件付きで必須にしたい | when() の then・otherwise を関数で書く |
when・ref・test |
| エラーを日本語にしたい | setLocale() と label() |
日本語にする |
| TypeScript の型を作りたい | yup.InferType<typeof schema> |
TypeScriptでの型 |
| yup zod 違い | 手元の実測では Zod のほうが数倍〜十数倍速い。Yup は変換と when が書きやすい | Yup・Zod・Valibot・Joiの違い |
| React Hook Form で使う | yupResolver(schema) を resolver に渡す |
React Hook Form・Formik |
確認環境:Ubuntu 24.04.5 LTS・Node.js 22.23.4 の上で、作業用のフォルダーに yup 1.7.1 を npm で入れ、この記事のコードをすべて実行して出力をそのまま載せています(タイムゾーンは Asia/Tokyo)。比較には zod 4.6.7(通常版と zod/mini)・valibot 1.5.0・joi 18.3.1 を使い、バンドルの大きさは esbuild 0.28.2(--bundle --minify)と gzip -9、速さは同じデータを2万回ずつ検証して3回測りました。型は TypeScript 7.0.2(tsc --strict)で確かめています。フォームは React 19.3.1・react-hook-form 7.91.0・@hookform/resolvers 6.0.2・formik 2.4.9 を Vite 8.3.5 でビルドして 127.0.0.1 で配信し、Chromium 153.0.8010.12(Playwright 1.63.0)で入力・送信して表示を確かめました。API の説明は Yup の公式 README(GitHub)と npm のページで確かめています。図は mermaid 11.16.0 で描画を確かめました(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Yupとは・使い方|エラー原文とZod比較の実測つき」
Yupとは(何をするバリデーションライブラリか)
この節では、Yup が何をするライブラリで、どこで使われるのかが分かります。
Yup は、値の「形」と「決まり」をスキーマとして書き、その値を変換(parse)してから検証(validate)するライブラリです。公式 README の冒頭は「Yup is a schema builder for runtime value parsing and validation.」(実行時に値を解析・検証するためのスキーマビルダー)です。
「スキーマ」とは、「name は20文字以内の文字列で必須」「age は18以上の整数」のように、データが満たすべきルールをまとめた定義です。TypeScript の型はビルド時にしか働かず、フォームの入力や API の応答のように実行時に外から入ってくる値は確かめられません。そこを埋めるのが Yup のようなスキーマバリデーションです。
Yup の特徴は次の4つです。
変換してから検証
文字列の "20" を数値の 20 に、" 山田 " を "山田" に変えてから検証する。
フォームの値と相性がよい
メソッドをつなげて書く
yup.string().email().required() のように、ルールを鎖のようにつなぐ。
読みやすい
項目どうしの関係
when() で「配送なら住所が必須」、ref() で「確認用パスワードが一致」を書ける。
条件付きの検証
非同期の検証が標準
validate() は Promise を返す。サーバーへの問い合わせを含むルールも書ける。
同期版もある
なお、HTML の required や pattern 属性だけで済む小さなフォームもあります。ブラウザ標準の検証は、関連記事「HTMLフォームバリデーション|required・pattern早見表と完成例」で扱っています。この記事は、JavaScript 側でルールを持ちたいときの Yup に絞ります。
Yupのインストールと最初のバリデーション
この節では、Yup を入れて最初のスキーマを動かすまでの手順と、その出力が分かります。
npm で入れて、import * as yup from 'yup' で読み込みます。型定義は本体に入っているので、TypeScript 用の別パッケージは要りません。
npm install yup
# yarn add yup / pnpm add yup でも同じ
スキーマを作る
yup.object({ ... }) の中に、項目名とルールを並べる。
値を渡して検証する
await schema.validate(value)。合格なら変換後の値が返る。
不合格を受け取る
try/catch で ValidationError を受け、errors や inner を画面に出す。
会員登録フォームを想定したスキーマです。
import * as yup from 'yup';
const userSchema = yup.object({
name: yup.string().trim().required('名前は必須です').max(20),
email: yup.string().email('メールアドレスの形式が正しくありません').required(),
age: yup.number().integer().min(18, '18歳以上が対象です').required(),
plan: yup.string().oneOf(['free', 'pro'], 'free か pro を選んでください').default('free'),
zip: yup.string().matches(/^\d{3}-\d{4}$/, '郵便番号は 123-4567 の形です'),
tags: yup.array(yup.string()).max(3),
birthday: yup.date().max(new Date('2026-10-29')),
});
const ok = await userSchema.validate({
name: ' 山田 ', email: 'a@example.com', age: '20', tags: ['x'], birthday: '2000-01-02',
});
console.log('OK:', JSON.stringify(ok));
OK: {"birthday":"2000-01-01T15:00:00.000Z","tags":["x"],"plan":"free","age":20,"email":"a@example.com","name":"山田"}
出力から、Yup の動きが3つ読み取れます。
1値が変換されて返る
文字列の "20" が数値の 20 に、前後の空白が trim で消えた。返り値は入力そのものではなく、変換後の値。
2default が入る
渡していない plan に、default の "free" が入った。zip のような任意の項目は、渡さなければそのまま無い。
3日付は地域の時刻で読まれる
"2000-01-02" は日本時間の0時として読まれ、UTC では前日の15時になった。サーバーへ送る前に注意がいる。
Yupのスキーマの種類:string・number・boolean・date・array・object
この節では、Yup で使えるスキーマの種類と、よく使うメソッドの一覧が分かります。
スキーマは値の型ごとに分かれ、それぞれに専用のメソッドがあります。どの型にも共通で使えるのが required()・optional()・nullable()・default()・oneOf()・test()・transform() です。
| スキーマ | よく使うメソッド | 手元で確かめた動き |
|---|---|---|
string() |
min・max・length・email・url・uuid・matches・trim・lowercase・datetime | 数値 123 は文字列 "123" に変換されて通った |
number() |
min・max・integer・positive・negative・moreThan・lessThan | "20" は 20 に変換。"abc" と空文字は NaN になり型エラー |
boolean() |
oneOf([true])・isTrue | 規約の同意チェックに使う |
date() |
min・max | "2026-10-29" は日本時間の0時として読まれた |
array() |
of・min・max・length・compact | 中の要素のエラーは items[1].qty の形の path になる |
object() |
shape・pick・omit・partial・concat・noUnknown・exact | 知らないキーは既定では残る |
mixed()・tuple()・lazy() |
型を問わない値・長さ固定の配列・値によってスキーマを変える | tuple は要素ごとに変換。長すぎるとエラー |
required・optional・nullable・defined の違い
「値が無い」には undefined・null・空文字の3つがあり、Yup はそれぞれ扱いが違います。手元で確かめた結果です。
await yup.string().validate(undefined); // → undefined(既定は任意なので通る)
await yup.string().validate(null); // → this cannot be null
await yup.string().nullable().validate(null); // → null
await yup.string().defined().validate(undefined);// → this must be defined
await yup.string().required().validate(''); // → this is a required field
通る(エラーにならない)
何も付けないスキーマに undefined
nullable() を付けて null
required() の無い string に空文字 ""
エラーになる
何も付けないスキーマに null
defined() に undefined
required() に undefined・null・空文字
知らないキーの扱い(stripUnknown・noUnknown・exact)
スキーマに無いキーが入った値は、既定ではそのまま残ります。API にそのまま渡すと、想定外の項目まで保存されることがあります。
unknown kept: {"a":"x","b":1}
stripUnknown: {"a":"x"}
noUnknown: {"a":"x"}
noUnknown+strict: this field has unspecified keys: b
exact: this object contains unknown properties: b
消したいなら validate(v, { stripUnknown: true }) か noUnknown()、エラーにしたいなら exact()(または noUnknown().strict())を使います。noUnknown() は strict でないときは「消す」動きになり、エラーにはなりませんでした。
validateとvalidateSyncの違い・abortEarlyでエラーを全部出す
この節では、検証メソッドの使い分けと、エラーを1件だけでなく全部受け取る方法が分かります。
validate() は Promise を返す非同期版、validateSync() は値をすぐ返す同期版です。どちらも既定では最初のエラーで止まり、abortEarly: false を付けると全部のエラーを集めます。
| メソッド | 返すもの | 向く場面 |
|---|---|---|
validate(v, opt) |
Promise(合格で値、不合格で reject) | 既定の選択。非同期の test を含むとき |
validateSync(v, opt) |
値(不合格は throw) | 非同期の test が無く、すぐ結果が欲しいとき |
isValid・isValidSync |
true・false | 合否だけ知りたいとき |
validateAt(path, v) |
1つの項目だけ検証 | 入力欄ごとの検証 |
cast(v) |
変換だけした値(検証しない) | 値をそろえたいだけのとき |
わざと5か所まちがえた値で、abortEarly の既定(true)と false を比べました。
const bad = { name: '', email: 'abc', age: 15, zip: '1234567', tags: ['a', 'b', 'c', 'd'] };
try { await userSchema.validate(bad); } catch (e) {
console.log(e.name, '|', e.message, '|', e.path, '|', e.type, '|', JSON.stringify(e.errors));
}
try { await userSchema.validate(bad, { abortEarly: false }); } catch (e) {
console.log(e.message);
console.log(JSON.stringify(e.errors, null, 1));
for (const i of e.inner) console.log(i.path, i.type);
}
ValidationError | tags field must have less than or equal to 3 items | tags | max | ["tags field must have less than or equal to 3 items"]
5 errors occurred
[
"名前は必須です",
"メールアドレスの形式が正しくありません",
"18歳以上が対象です",
"郵便番号は 123-4567 の形です",
"tags field must have less than or equal to 3 items"
]
name required
email email
age min
zip matches
tags max
abortEarly が既定のままだと、返ってきたのは定義順で最後の tags の1件だけでした。「最初に書いた項目のエラーが返る」とは限らないため、フォームでは abortEarly: false にして全部を出すのが基本です。
同期版で非同期の test を使うとエラーになる
後で出てくる非同期の test()(サーバーへの問い合わせなど)を含むスキーマを validateSync() で検証すると、次のエラーになりました。
Error Validation test of type: "unique" returned a Promise during a synchronous validate. This test will finish after the validate call has returned
非同期のルールが1つでもあるスキーマは、validate()(await)で検証します。
ValidationErrorの中身(エラーの読み方)
この節では、Yup が投げる ValidationError のどこを見れば、どの項目の何のエラーか分かるのかが分かります。
ValidationError は Error を継承したクラスで、path(どの項目か)・type(どのルールか)・message・errors・inner を持ちます。手元で Object.keys(e) を出すと次の8つでした。
keys: value,path,type,params,errors,inner,name,message
instanceof: true true
| プロパティ | 中身 | 使い道 |
|---|---|---|
path |
"email"・"items[1].qty" のような場所 | どの入力欄の下に出すか決める |
type |
"required"・"min"・"email" などルール名 | 種類ごとに表示を変える |
message |
1件なら本文、複数なら「5 errors occurred」 | ログ |
errors |
メッセージの配列 | 上にまとめて並べる |
inner |
1件ずつの ValidationError の配列 | 項目ごとのエラーに組み直す |
params |
min の値・正規表現など | 独自のメッセージを作る |
入力欄ごとに出すには、inner を path で組み直します。配列の中の要素は items[1].sku のような path になりました。
function toFieldErrors(err) {
const out = {};
for (const i of err.inner) out[i.path] ??= i.message; // 1項目につき最初の1件
return out;
}
const cart = yup.object({
items: yup.array(yup.object({ sku: yup.string().required(), qty: yup.number().min(1) })).min(1),
});
try {
await cart.validate({ items: [{ sku: 'A', qty: 1 }, { qty: 0 }] }, { abortEarly: false });
} catch (e) { console.log(e.inner.map((i) => i.path).join(', ')); }
items[1].sku, items[1].qty
エラーかどうかの判定は e instanceof yup.ValidationError か yup.ValidationError.isError(e) で行い、ほかの例外(プログラムの不具合)と混ぜないようにします。
castとtransform:Yupが値を変換する仕組み
この節では、Yup が検証の前に値を変換する仕組みと、それが原因で起きる意外な結果が分かります。
Yup は検証の前に、スキーマの型に合わせて値を変換(cast)します。変換を止めて「渡された値そのもの」で検証したいときは strict() か validate(v, { strict: true }) を使います。
cast: {"birthday":"2000-01-01T15:00:00.000Z","plan":"free","age":30,"email":"a@b.co","name":"a"}
strict: this must be a `number` type, but the final value was: `"20"`.
NaN: this must be a `number` type, but the final value was: `NaN` (cast from the value `"abc"`).
number "": this must be a `number` type, but the final value was: `NaN` (cast from the value `""`).
3行目と4行目がフォームでいちばん出会うエラーです。入力欄が空のとき、値は空文字 "" で、number() は NaN に変換してから型エラーにします。「必須です」ではなく英語の型エラーが出る原因です。
空の数値欄の扱い
yup.number().required('必須です') だけ。空欄で送ると「must be a `number` type」が出て、必須のメッセージにならないtransform で空文字を undefined に変え、typeError() で型エラーの文言も決める。空欄は「必須です」、文字は「数字で」と分かれる見分ける問い:「空欄のとき、どのメッセージを出したいか」
const age = yup.number()
.transform((value, original) => (original === '' ? undefined : value))
.typeError('年齢は数字で入力')
.integer('年齢は整数です')
.min(18, '18歳以上が対象です')
.required('年齢は必須です');
transform で全角数字を半角にする
transform(value, originalValue) は、変換の途中で値を書き換える関数です。全角の「123」を半角にしてから matches で検証すると、123 が返りました。
const digits = yup.string()
.transform((v) => typeof v === 'string'
? v.replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
: v)
.matches(/^\d+$/);
console.log(await digits.validate('123')); // → 123
日付は時刻のずれに気をつける
yup.date() は、"2026-10-29" と "2026-10-29T00:00:00" のどちらも日本時間の0時として読み、UTC では前日の 15:00 になりました。日付だけを扱いたいなら、yup.string().matches(/^\d{4}-\d{2}-\d{2}$/) で文字列のまま検証するか、string().datetime()(ISO 8601 の日時。"2026-10-29" だけだと「must be a valid ISO date-time」)を使う方法があります。
date: 2026-10-28T15:00:00.000Z 2026-10-28T15:00:00.000Z
datetime: this must be a valid ISO date-time
datetime ok: 2026-10-29T09:00:00Z
Yupのwhen・ref・test:条件付きと自作ルール
この節では、項目どうしの関係(条件付き必須・一致確認)と、自分で作るルールの書き方が分かります。
ほかの項目によってルールを変えるのが when()、ほかの項目の値を参照するのが ref()、好きな判定を足すのが test() です。
when:配送なら住所が必須
Yup 1.x では、then・otherwise にスキーマを受け取ってスキーマを返す関数を書きます。
const order = yup.object({
delivery: yup.string().oneOf(['shop', 'home']).required(),
address: yup.string().when('delivery', {
is: 'home',
then: (s) => s.required('配送先住所は必須です'),
otherwise: (s) => s.strip(), // 店舗受け取りなら住所は捨てる
}),
});
when shop: {"delivery":"shop"}
when home: 配送先住所は必須です
店舗受け取り(shop)では、渡した住所が strip() で返り値から消えています。0.x 系の書き方(then: yup.string().required() のようにスキーマを直接渡す)を 1.x で動かすと、TypeError: branch is not a function になりました。
ref:確認用パスワードの一致
const pw = yup.object({
password: yup.string().min(8).required(),
confirm: yup.string().oneOf([yup.ref('password')], 'パスワードが一致しません').required(),
});
// { password: 'abcdefgh', confirm: 'abcdefgX' } → パスワードが一致しません
test:自作のルール(同期・非同期)
test(名前, メッセージ, 判定関数) で、true を返せば合格です。判定関数を async にすると、サーバーへの問い合わせを含む検証も書けます。メッセージの ${value} には入力値が入ります。
const kana = yup.string().test('katakana', '全角カタカナのみ',
(v) => v == null || /^[ァ-ヶー ]+$/.test(v));
const taken = new Set(['admin']); // 例:登録済みの名前(本来は API に問い合わせる)
const username = yup.string().test('unique', '${value} は使われています', async (v) => {
await new Promise((r) => setTimeout(r, 50));
return !taken.has(v);
});
test: 全角カタカナのみ katakana
async test: admin は使われています
複数の項目をまとめて判定し、エラーを特定の項目に付けたいときは、object に test を付け、this.createError({ path, message }) を返します。アロー関数では this が使えないため、function で書きます。
const range = yup.object({ from: yup.number(), to: yup.number() })
.test('range', function (v) {
return v.from <= v.to
|| this.createError({ path: 'to', message: `終了(${v.to})は開始(${v.from})以上にします` });
});
// { from: 10, to: 3 } → path: to / 終了(3)は開始(10)以上にします
addMethod・lazy・pick/omit
yup.addMethod(yup.string, 'zip', fn):何度も使うルールをyup.string().zip()の形で呼べるようにする。TypeScript では型の宣言(後述)も要る。yup.lazy((v) => ...):値を見てからスキーマを決める。手元では数値ならnumber().min(0)、文字列ならstring().max(3)に切り替わった。pick・omit・partial・concat:登録用のスキーマから、更新用(全部任意)やログイン用(一部だけ)を作る。partial()にすると name だけの値でも合格した。
Yupのエラーメッセージを日本語にする(setLocale・label)
この節では、英語の既定のエラーメッセージを日本語にまとめて差し替える方法が分かります。
yup.setLocale() で既定のメッセージを一括で差し替え、label() で項目名を日本語にします。メッセージの中では ${path}・${label}・${min}・${max}・${values} が使え、関数で書くこともできます。setLocale はスキーマを作る前に1回だけ呼びます。
yup.setLocale({
mixed: {
required: ({ label, path }) => `${label ?? path}は必須です`,
notType: ({ label, path, type }) =>
`${label ?? path}は${type === 'number' ? '数値' : type}で入力してください`,
oneOf: '${path}は次のいずれかです: ${values}',
},
string: {
email: 'メールアドレスの形式で入力してください',
min: '${label}は${min}文字以上で入力してください',
max: '${label}は${max}文字以内で入力してください',
},
number: { min: '${label}は${min}以上にしてください', integer: '${label}は整数で入力してください' },
});
const s = yup.object({
name: yup.string().label('氏名').required().max(10),
password: yup.string().label('パスワード').min(8).required(),
age: yup.number().label('年齢').integer().min(18),
email: yup.string().label('メール').email().required(),
plan: yup.string().oneOf(['free', 'pro']),
});
氏名は10文字以内で入力してください
パスワードは8文字以上で入力してください
年齢は数値で入力してください
メールアドレスの形式で入力してください
planは次のいずれかです: free, pro
---
氏名は必須です
年齢は整数で入力してください
年齢は18以上にしてください
メールは必須です
2つ目の結果では、17.5 歳に対して「整数」と「18以上」の2件が出ています。abortEarly: false では、1つの項目に複数のエラーが出ることがあります。画面に1件だけ出したいなら、path ごとに最初の1件を取ります(前の節の toFieldErrors)。label を付けない項目(plan)は、キー名がそのまま出ます。
TypeScriptでのYupの型:InferTypeとObjectSchema
この節では、スキーマから TypeScript の型を作る方法と、既存の型にスキーマを合わせる方法が分かります。
yup.InferType<typeof schema> で、スキーマから型を取り出せます。公式 README は、型推論には tsconfig の strictNullChecks(strict に含まれる)が必須だと書いています。
import * as yup from 'yup';
const userSchema = yup.object({
name: yup.string().required(),
age: yup.number().integer().min(0),
nick: yup.string().nullable(),
plan: yup.string().oneOf(['free', 'pro'] as const).default('free'),
tags: yup.array(yup.string().required()).default([]),
});
type User = yup.InferType<typeof userSchema>;
const bad: User = { name: 'a', plan: 'gold', tags: [] }; // plan が違う
const missing: User = { age: 3, plan: 'pro', tags: [] }; // name が無い
tsc --strict のエラーに、推論された型がそのまま出ました。
error TS2322: Type '"gold"' is not assignable to type '"free" | "pro"'.
error TS2741: Property 'name' is missing in type '{ age: number; plan: "pro"; tags: never[]; }' but required in type '{ age?: number | undefined; name: string; nick?: string | null | undefined; plan: "free" | "pro"; tags: string[]; }'.
- required の無い項目は
?(任意)になり、nullable()を付けると| nullが加わる。 oneOf([...] as const)にすると、string ではなく"free" | "pro"になる。- default を付けた項目は、返り値に必ず入るので任意にならない。
既存の型にスキーマを合わせる(ObjectSchema)
先に型があるときは const schema: yup.ObjectSchema<Form> = yup.object({...}) と書くと、スキーマと型のずれがコンパイルエラーになります。age を number の型なのに yup.string() で書いた例では、次のエラーが出ました(抜粋)。
error TS2322: Type 'ObjectSchema<{ email: string; age: string | undefined; }, AnyObject, { email: undefined; age: undefined; }, "">' is not assignable to type 'ObjectSchema<Form, AnyObject, any, "">'.
Type 'string' is not assignable to type 'number'.
addMethod の型を宣言する
addMethod で足したメソッドは、宣言しないと Property 'zip' does not exist on type 'StringSchema<...>'(TS2339)になります。次の宣言を足すとエラーが消えました。
declare module 'yup' {
interface StringSchema<TType, TContext, TDefault, TFlags> {
zip(msg?: string): this;
}
}
yup.addMethod(yup.string, 'zip', function (msg = '郵便番号の形式が違います') {
return this.matches(/^\d{3}-?\d{4}$/, msg);
});
型の取り出し方は、Zod の z.infer とほぼ同じ考え方です。TypeScript の型の絞り込み全般は、関連記事「TypeScript型ガード|is・in・assertsの使い分け一覧」で扱っています。
React Hook Form・FormikでYupを使う(ブラウザで確かめた結果)
この節では、React のフォームライブラリと Yup を組み合わせる最小のコードと、Chromium で動かした結果が分かります。
React Hook Form は @hookform/resolvers/yup の yupResolver(schema) を、Formik は validationSchema={schema} を渡すだけで Yup を使えます。同じスキーマを両方で使いました。
// schema.js(2つのフォームで共通)
export const schema = yup.object({
name: yup.string().trim().required('氏名は必須です').max(20, '氏名は20文字以内です'),
email: yup.string().required('メールは必須です').email('メールの形式が正しくありません'),
age: yup.number().transform((v, o) => (o === '' ? undefined : v))
.typeError('年齢は数字で入力').integer('年齢は整数です')
.min(18, '18歳以上が対象です').required('年齢は必須です'),
agree: yup.boolean().oneOf([true], '利用規約への同意が必要です'),
});
// React Hook Form
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
export default function SignupForm({ onDone }) {
const { register, handleSubmit, formState: { errors } } = useForm({ resolver: yupResolver(schema) });
return (
<form onSubmit={handleSubmit(onDone)} noValidate>
<label>氏名 <input {...register('name')} /></label><p>{errors.name?.message}</p>
<label>メール <input {...register('email')} /></label><p>{errors.email?.message}</p>
<label>年齢 <input {...register('age')} /></label><p>{errors.age?.message}</p>
<label><input type="checkbox" {...register('agree')} /> 同意する</label><p>{errors.agree?.message}</p>
<button>送信</button>
</form>
);
}
// Formik
import { Formik, Form, Field, ErrorMessage } from 'formik';
export default function SignupFormik({ onDone }) {
return (
<Formik initialValues={{ name: '', email: '', age: '', agree: false }}
validationSchema={schema} onSubmit={(v) => onDone(schema.cast(v))}>
<Form noValidate>
<label>氏名 <Field name="name" /></label><p><ErrorMessage name="name" /></p>
<label>メール <Field name="email" /></label><p><ErrorMessage name="email" /></p>
<label>年齢 <Field name="age" /></label><p><ErrorMessage name="age" /></p>
<label><Field type="checkbox" name="agree" /> 同意する</label><p><ErrorMessage name="agree" /></p>
<button type="submit">送信</button>
</Form>
</Formik>
);
}
Vite でビルドして 127.0.0.1 で配信し、Playwright で Chromium を動かして、空のまま送信 → まちがった値 → 正しい値の順に入力しました。両方とも表示は同じでした。
| 操作 | 表示されたエラー(両方同じ) | 送信された値 |
|---|---|---|
| 空のまま送信 | 氏名は必須です/メールは必須です/年齢は必須です/利用規約への同意が必要です | 送信されない |
| 「 山田 」「taro@」「17.5」 | メールの形式が正しくありません/年齢は整数です/利用規約への同意が必要です | 送信されない |
| 正しい値と同意 | なし | RHF:age は数値 20、name は "山田" |
違いが出たのは、送信時に受け取る値です。React Hook Form の yupResolver は、Yup が変換したあとの値(数値の 20、trim 後の "山田")を onSubmit に渡しました。Formik は入力されたままの値を渡し、schema.cast() を通して初めて変換後の値になりました。
{"raw":{"name":" 山田 ","email":"taro@example.com","age":"20","agree":true},
"cast":{"agree":true,"age":20,"email":"taro@example.com","name":"山田"}}
ビルドした JavaScript は、React・両ライブラリ・Yup を合わせて 337.75 kB(gzip 106.55 kB)でした。React Hook Form の register・Controller・mode などの使い方は、関連記事「React Hook Formの使い方|zod v4連携のコピペ例とエラー対処」で扱っています。resolver を zodResolver から yupResolver に替えれば、同じ組み立てで Yup を使えます。
Yup・Zod・Valibot・Joiの違い(同じスキーマで実測比較)
この節では、よく比べられる4つのバリデーションライブラリの違いを、同じ会員登録のスキーマで書いて測った結果で分かります。
同じ7項目(名前・メール・年齢・パスワード・確認用・プラン・タグ)のスキーマを4つのライブラリで書き、書く量・バンドルの大きさ・速さを測りました。Zod は通常版と、小さく作れる zod/mini の2通りです。
// Yup
yup.object({
name: yup.string().trim().min(1).max(20).required(),
email: yup.string().email().required(),
age: yup.number().integer().min(18).required(),
password: yup.string().min(8).required(),
confirm: yup.string().oneOf([yup.ref('password')]).required(),
plan: yup.string().oneOf(['free', 'pro']).required(),
tags: yup.array(yup.string().required()).max(3).required(),
});
// Zod 4
z.object({
name: z.string().trim().min(1).max(20),
email: z.email(),
age: z.number().int().min(18),
password: z.string().min(8),
confirm: z.string(),
plan: z.enum(['free', 'pro']),
tags: z.array(z.string()).max(3),
}).refine((v) => v.password === v.confirm, { path: ['confirm'] });
// Valibot
v.pipe(v.object({
name: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(20)),
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(18)),
password: v.pipe(v.string(), v.minLength(8)),
confirm: v.string(),
plan: v.picklist(['free', 'pro']),
tags: v.pipe(v.array(v.string()), v.maxLength(3)),
}), v.forward(v.check((i) => i.password === i.confirm), ['confirm']));
// Joi
Joi.object({
name: Joi.string().trim().min(1).max(20).required(),
email: Joi.string().email({ tlds: { allow: false } }).required(),
age: Joi.number().integer().min(18).required(),
password: Joi.string().min(8).required(),
confirm: Joi.any().valid(Joi.ref('password')).required(),
plan: Joi.string().valid('free', 'pro').required(),
tags: Joi.array().items(Joi.string()).max(3).required(),
});
書く量とバンドルの大きさ
各スキーマで1回検証するだけのファイルを esbuild(--bundle --minify、ブラウザ向け)でまとめ、gzip -9 で圧縮した大きさです。書く量は、スキーマ部分の空白と改行を除いた文字数です。
| ライブラリ(版) | スキーマの文字数 | minify 後 | gzip 後 |
|---|---|---|---|
| Yup 1.7.1 | 400 | 42,465 B | 13,492 B |
| Zod 4.6.7(通常版) | 294 | 454,191 B | 92,502 B |
| Zod 4.6.7(zod/mini) | 375 | 20,179 B | 6,943 B |
| Valibot 1.5.0 | 414 | 6,028 B | 2,078 B |
| Joi 18.3.1 | 407 | 174,012 B | 55,449 B |
Zod の通常版が大きいのは、esbuild の内訳を見ると、JSON Schema との変換や各言語のメッセージ(locales)まで一緒に入っていたためです。書く量はどれも大きくは変わらず、大きさは Valibot・zod/mini・Yup の順に小さい結果でした。
検証の速さ
正しい値(good)と、7項目すべてがまちがった値(bad)を、それぞれ2万回検証した1秒あたりの回数です。Yup は validateSync(abortEarly: false)、Zod・Valibot は safeParse、Joi は validate を使いました。同じマシンで3回測った幅を載せます(2コアの共用マシンのため、数値は目安です)。
| ライブラリ | 正しい値(回/秒) | まちがった値(回/秒) | エラー件数 |
|---|---|---|---|
| Yup | 約5.1万〜5.5万 | 約3,300〜3,900 | 9 |
| Zod(通常版) | 約59万〜95万 | 約18万〜21万 | 6 |
| Zod(zod/mini) | 約43万〜66万 | 約11万〜15万 | 6 |
| Valibot | 約83万〜101万 | 約33万〜46万 | 7 |
| Joi | 約12万〜14万 | 約4.2万〜5.6万 | 8 |
- Yup は正しい値で Zod の10分の1以下、まちがった値ではさらに差が開きました(約50倍)。エラーごとに ValidationError(Error のオブジェクト)を作る分が重いと見られます。Yup の非同期の
validateは、正しい値で約0.9万〜3.2万回/秒でした。 - それでも1回あたりは正しい値で約0.02ミリ秒、まちがった値でも約0.3ミリ秒です。フォーム1回の送信で体感できる差ではなく、差が効くのは大量のデータや API の応答を何万件も検証する場面です。
- エラー件数の違いは、Zod の
refineと Valibot のforwardが「ほかの項目が通ってから」動くこと、Yup は name に min と required の2件を出したことによります。
同じまちがった値に対する、各ライブラリの最初の3件のメッセージ(既定の英語)です。
YUP 9
name | name must be at least 1 characters
name | name is a required field
email | email must be a valid email
ZOD 6
name | Too small: expected string to have >=1 characters
email | Invalid email address
age | Invalid input: expected int, received number
VALIBOT 7
name | Invalid length: Expected >=1 but received 0
email | Invalid email: Received "taro"
age | Invalid integer: Received 15.5
JOI 8
name | "name" is not allowed to be empty
email | "email" must be a valid email
age | "age" must be an integer
| 観点 | Yup | Zod | Valibot・Joi |
|---|---|---|---|
| 考え方 | 変換してから検証 | 型として解析(変換は明示) | Valibot は関数を組み合わせる。Joi は Node 向けの検証 |
| 型の取り出し | InferType | z.infer | Valibot は InferOutput。Joi はスキーマから型を取り出す仕組みを持たない |
| 非同期の検証 | validate が標準で非同期 | parseAsync で明示 | Valibot は parseAsync。Joi は validateAsync |
| Standard Schema | 対応(README に記載) | 対応 | Valibot 1.5.0・Joi 18.3.1 も対応(手元で確かめた) |
Yup 1.7.1 のスキーマには ~standard のプロパティがあり(vendor は "yup")、Standard Schema に対応したライブラリからそのまま使えることを確かめました。zod・valibot・joi のスキーマにも同じプロパティがありました。React Hook Form の resolvers には、どのライブラリでも使える standardSchemaResolver もあります。Zod の書き方とエラーの出方は、関連記事「Zodの使い方|v4対応のAPI早見表とエラー出力つき実例3つ」で扱っています。
Yupを選ぶ・選ばない判断表
この節では、新しく作るとき・すでに Yup があるときに、Yup を使うかどうかを決める目安が分かります。
すでに Yup で書かれたフォームを保守するなら、無理に移す理由は少なく、新しく TypeScript で作るなら Zod か Valibot も候補に入れる、というのが手元の実測からの目安です。
| 状況 | 向く選択 | 理由(この記事の実測) |
|---|---|---|
| Formik・Yup で作られた既存のフォーム | Yup のまま | Formik は validationSchema に Yup をそのまま受け取る |
| 文字列の入力を数値・日付に変えて検証したい | Yup | cast で "20" → 20 になる。transform も書きやすい |
| 項目どうしの条件が多いフォーム | Yup(when)か Zod(refine) | when で then・otherwise を項目に直接書ける |
| 新規の TypeScript のプロジェクト | Zod | 型の推論と資料が多い。速さは Yup の10倍以上 |
| ブラウザに送る JS を小さくしたい | Valibot か zod/mini | gzip 後 2.1KB・6.9KB(Yup は 13.5KB) |
| API で大量のデータを検証する | Valibot か Zod | まちがった値で Yup の50倍以上の回数を処理 |
| Node のサーバーだけで使う | Joi か Zod | 大きさが問題になりにくい |
移すか迷ったときは、次の順に確かめると決めやすくなります。
- 1いまのスキーマが困っていないか困っていなければ Yup のまま。移す手間とテストの書き直しが要る
- 2型とスキーマが二重になっていないかInferType に寄せれば、Yup のままでも一つにできる
- 3検証の回数が多い処理があるか一括取り込み・API の応答なら、その部分だけ別のライブラリにする手もある
- 4送る JS の大きさが問題かビルドの内訳で、バリデーションの割合を測ってから決める
Python 側で同じ考え方(スキーマで変換と検証)をするのが Pydantic です。サーバーを Python で書くなら、関連記事「Pydanticとは|何に使う・dataclassとの違いと使い方を実測で解説」で扱っています。
Yupのよくあるエラーとつまずき
この節では、Yup で出会いやすいエラーの原文と、その原因と直し方が分かります。
多くは「値が変換される」「既定は任意」「1.x で書き方が変わった」の3つが原因です。原文はすべて手元で出したものです。
| エラー原文(抜粋) | 原因 | 直し方 |
|---|---|---|
must be a `number` type, but the final value was: `NaN` |
空欄("")や文字を number() に渡した | transform で "" を undefined に。typeError() で文言を決める |
TypeError: branch is not a function |
when の then にスキーマを直接渡した(0.x の書き方) | then: (s) => s.required() の関数にする |
returned a Promise during a synchronous validate |
非同期の test を validateSync で検証した | await validate() にする |
this cannot be null |
null を許していない | nullable() を付ける |
this must be a `string` type, but the final value was: `123` |
strict() で変換が止まっている | 変換してよいなら strict を外す |
Property 'zip' does not exist on type(TS2339) |
addMethod の型の宣言が無い | declare module 'yup' で宣言する |
must be a valid ISO date-time |
string().datetime() に日付だけを渡した | 時刻まで渡すか、matches で日付の形を確かめる |
5 errors occurred が画面に出る |
abortEarly: false の message をそのまま表示した | inner を path ごとに組み直す |
エラーにならないのに気づきにくいもの
- 1email() は "a@b" を通す手元で a@b は合格した。ドメインに「.」を必須にしたいなら matches を足す
- 2required の無い項目は undefined で通るemail() や min() を付けても、値が無ければ検証されない
- 3スキーマに無いキーは残る保存の前に stripUnknown か exact() を使う
- 4date() は地域の時刻で読む"2026-10-29" が UTC では前日 15:00 になった
- 5Formik の onSubmit は変換前の値数値にしたいなら schema.cast() を通す
なお、ブラウザ側の検証は送る前の案内であり、サーバー側でも同じルールで検証します。Node のサーバーなら、同じ Yup のスキーマを共有できます。
よくある質問
Q. Yupとは何ですか? A. JavaScript・TypeScript で、値の形と決まりをスキーマとして書き、実行時に変換と検証を行うライブラリです。フォームの入力や API の値を確かめるのに使います。ライセンスは MIT です。
Q. 配列やネストしたオブジェクトのエラーはどこに出ますか?
A. path が items[1].qty のように、配列の番号とキーをつないだ形になります。abortEarly: false で検証し、e.inner の path を見れば、どの行のどの項目かが分かります。
Q. Yup の使い方を一言で言うと?
A. yup.object({ 項目: yup.string().required() }) でスキーマを作り、await schema.validate(値, { abortEarly: false }) で検証して、失敗したら e.inner を読みます。
Q. Yup と Zod の違いは何ですか? A. Yup は「変換してから検証」が基本で、when で条件付きの検証を書きやすいライブラリです。Zod は型の推論が中心で、手元の実測では正しい値で10倍以上、まちがった値で50倍前後速く検証しました。バンドルは Zod の通常版が大きく、zod/mini なら Yup より小さくなりました。
Q. Yup はまだ使われていますか?開発は続いていますか? A. npm の最新版は 1.7.1(2025年9月公開)で、2026年10月29日時点の latest です。Formik や React Hook Form の resolver からも引き続き使えます。
Q. エラーメッセージを日本語にするには?
A. スキーマを作る前に yup.setLocale() で既定のメッセージを差し替え、各項目に label('氏名') を付けます。個別のルールには第2引数(例:required('氏名は必須です'))で直接書けます。
Q. 数値の入力欄が空のとき、英語の型エラーが出ます。
A. 空欄は空文字 "" で、number() が NaN に変換するためです。transform((v, o) => o === '' ? undefined : v) で undefined にし、required() と typeError() で文言を分けます。
Q. パスワードと確認用の一致はどう書きますか?
A. 確認用の項目に oneOf([yup.ref('password')], '一致しません') を付けます。
Q. TypeScript で型を作るには?
A. type User = yup.InferType<typeof userSchema> です。tsconfig で strict(strictNullChecks)が有効である必要があります。既存の型に合わせるときは yup.ObjectSchema<型> を注釈に使います。
Q. React Hook Form で使うには?
A. @hookform/resolvers を入れ、useForm({ resolver: yupResolver(schema) }) とします。エラーは formState.errors.項目.message に入ります。
Q. サーバー(Node.js)でも使えますか? A. 使えます。ブラウザと同じスキーマを共有すれば、ルールを1か所にまとめられます。大量のデータを検証するなら、速さの差も考えてライブラリを選びます。
まとめ
- Yup は、値をスキーマに合わせて変換(cast)してから検証するライブラリ。最新版は 1.7.1(2026年10月29日時点)。
- フォームでは
abortEarly: falseで全部のエラーを集め、innerを path ごとに組み直して入力欄の下に出す。 - 空の数値欄は NaN になって英語の型エラーが出る。transform と typeError で文言を分ける。1.x の when は then・otherwise を関数で書く。
- 日本語化は setLocale と label、型は InferType。React Hook Form は yupResolver が変換後の値を渡し、Formik は変換前の値を渡した。
- 同じスキーマの実測では、Zod は10倍以上、Valibot はさらに速く、gzip 後の大きさは Valibot 2.1KB・zod/mini 6.9KB・Yup 13.5KB・Joi 55KB・Zod 通常版 93KB。既存の Yup はそのまま、新規は Zod・Valibot も候補にする。
参考・出典
いずれも確認日は 2026年10月29日です。
- Yup(GitHub・公式 README):https://github.com/jquense/yup
- yup(npm):https://www.npmjs.com/package/yup
- React Hook Form「Resolvers」(@hookform/resolvers):https://github.com/react-hook-form/resolvers
- Formik「Validation」:https://formik.org/docs/guides/validation
- Zod(公式ドキュメント):https://zod.dev/
- Valibot(公式ドキュメント):https://valibot.dev/
- Joi(公式ドキュメント):https://joi.dev/
- Standard Schema:https://standardschema.dev/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Yupとは・使い方|エラー原文とZod比較の実測つき」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。