結論

Yup は、JavaScript・TypeScript で「入力値がこの形・この決まりに合っているか」をスキーマ(ルールの定義)で確かめるバリデーションライブラリです。値の変換(cast)と検証を一度に行い、フォームでは React Hook Form や Formik と組み合わせて使います。

この記事でわかること

  • string・number・when・test・transform など主な書き方と実行結果
  • ValidationError の中身、日本語のエラーメッセージ、InferType の型
  • 同じスキーマで Zod・Valibot・Joi と比べたコード量・サイズ・速さの実測

2026年10月29日更新|yup 1.7.1・Node.js 22・Chromium 153で実行確認

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 のようなスキーマバリデーションです。

1.7.1最新版(latest)npm・2026年10月29日時点
13.5KBgzip 後の大きさこの記事のスキーマを esbuild でまとめた実測
MITライセンスnpm の表示

Yup の特徴は次の4つです。

1

変換してから検証

文字列の "20" を数値の 20 に、" 山田 " を "山田" に変えてから検証する。

フォームの値と相性がよい

2

メソッドをつなげて書く

yup.string().email().required() のように、ルールを鎖のようにつなぐ。

読みやすい

3

項目どうしの関係

when() で「配送なら住所が必須」、ref() で「確認用パスワードが一致」を書ける。

条件付きの検証

4

非同期の検証が標準

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 でも同じ
1

スキーマを作る

yup.object({ ... }) の中に、項目名とルールを並べる。

object・string・number

2

値を渡して検証する

await schema.validate(value)。合格なら変換後の値が返る。

validate・validateSync

3

不合格を受け取る

try/catch で ValidationError を受け、errors や inner を画面に出す。

ValidationError

会員登録フォームを想定したスキーマです。

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. 1いまのスキーマが困っていないか困っていなければ Yup のまま。移す手間とテストの書き直しが要る
  2. 2型とスキーマが二重になっていないかInferType に寄せれば、Yup のままでも一つにできる
  3. 3検証の回数が多い処理があるか一括取り込み・API の応答なら、その部分だけ別のライブラリにする手もある
  4. 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ
参加受付中

AI駆動開発、 無料セミナー