Zod vs Yup — TypeScriptバリデーションライブラリ徹底比較
1. 結論
TypeScriptを主軸とした開発であればZodを選ぶべきです。 Zodはスキーマ定義から型を自動推論できるため、型定義の二重管理が不要になります。一方、既存のJavaScriptプロジェクトやFormikとの連携が前提であれば、Yupも依然として有力な選択肢です。 新規プロジェクトでは、エコシステムの成長速度と型安全性の観点からZodを第一候補にすることを推奨します。
2. 比較表
| 項目 | Zod | Yup |
|---|---|---|
| GitHub Stars(2025年時点) | 約 36,000+ | 約 23,000+ |
| npm 週間DL数 | 約 2,500万+ | 約 800万+ |
| バンドルサイズ(minified + gzip) | 約 14 kB | 約 13 kB |
| TypeScript対応 | ✅ TypeScript-first(TSで書かれている) | ⚠️ 型定義あり(後付け対応) |
| 型推論(スキーマ → 型) | ✅ z.infer<typeof schema> | ⚠️ InferType<typeof schema>(制限あり) |
| API設計 | イミュータブル・関数型寄り | メソッドチェーン・OOP寄り |
| 非同期バリデーション | ✅ .refine() / .superRefine() | ✅ .test() |
| カスタムエラーメッセージ | ✅ 柔軟に設定可能 | ✅ 柔軟に設定可能 |
| i18n(国際化) | コミュニティライブラリで対応 | コミュニティライブラリで対応 |
| Formikとの連携 | アダプター経由で可能 | ✅ 公式サポート |
| React Hook Formとの連携 | ✅ @hookform/resolvers 公式対応 | ✅ @hookform/resolvers 公式対応 |
| tRPC / Next.js連携 | ✅ ファーストクラスサポート | ⚠️ 非公式 |
| 学習コスト | 中(独自APIの理解が必要) | 低(直感的なAPI) |
| 初回リリース | 2020年 | 2014年 |
3. それぞれの強み
Zodの強み
① 完全な型推論
Zodの最大の強みは、スキーマ定義から TypeScript の型を完全に推論できる点です。型定義とバリデーションロジックが常に同期するため、「型は通るがバリデーションで落ちる」といった不整合が原理的に発生しません。
② イミュータブルなAPI設計
すべてのメソッドが新しいスキーマインスタンスを返すため、副作用を気にせずスキーマを合成・拡張できます。
③ モダンなエコシステムとの親和性
tRPC、Next.js(Server Actions)、Conform、Hono など、2020年代のモダンなフレームワークがZodをファーストクラスでサポートしています。
④ discriminatedUnion / branded types など高度な型表現
TypeScriptの高度な型パターンに対応しており、複雑なドメインモデルも表現できます。
Yupの強み
① 直感的で読みやすいAPI
メソッドチェーンベースのAPIは、バリデーションルールを英語のように読めるため、チーム内の非TypeScriptエンジニアにも理解しやすいです。
② 長い実績と安定性
2014年から開発が続いており、本番環境での実績が豊富です。枯れたライブラリとしての信頼性があります。
③ Formikとのネイティブ連携
Formikを使ったフォーム開発では、Yupスキーマをそのまま validationSchema に渡せるため、追加設定が不要です。
④ 条件付きバリデーションの書きやすさ
when() メソッドによる条件分岐が直感的で、フォームの動的バリデーションに強みがあります。
4. コード例で比較
ユーザー登録フォームのバリデーション
同じ要件を両ライブラリで実装します。
要件:
name: 必須、2〜50文字email: 必須、メール形式age: 任意、18以上120以下の整数role:"admin"|"user"のいずれかpassword: 必須、8文字以上、英数字混在
Zodの場合
import { z } from "zod";
// スキーマ定義
const userSchema = z.object({
name: z
.string()
.min(2, "名前は2文字以上で入力してください")
.max(50, "名前は50文字以内で入力してください"),
email: z
.string()
.email("有効なメールアドレスを入力してください"),
age: z
.number()
.int("年齢は整数で入力してください")
.min(18, "18歳以上である必要があります")
.max(120, "有効な年齢を入力してください")
.optional(),
role: z.enum(["admin", "user"]),
password: z
.string()
.min(8, "パスワードは8文字以上で入力してください")
.regex(
/^(?=.*[a-zA-Z])(?=.*\d)/,
"パスワードは英字と数字の両方を含めてください"
),
});
// スキーマから型を自動推論(型定義の二重管理が不要)
type User = z.infer<typeof userSchema>;
// => {
// name: string;
// email: string;
// age?: number | undefined;
// role: "admin" | "user";
// password: string;
// }
// バリデーション実行
const result = userSchema.safeParse({
name: "田中太郎",
email: "tanaka@example.com",
age: 30,
role: "admin",
password: "securePass1",
});
if (result.success) {
// result.data は User 型として推論される
console.log("有効なデータ:", result.data);
} else {
// エラー情報を構造化して取得できる
console.error("バリデーションエラー:", result.error.issues);
}
Yupの場合
import * as yup from "yup";
// スキーマ定義
const userSchema = yup.object({
name: yup
.string()
.required("名前は必須です")
.min(2, "名前は2文字以上で入力してください")
.max(50, "名前は50文字以内で入力してください"),
email: yup
.string()
.required("メールアドレスは必須です")
.email("有効なメールアドレスを入力してください"),
age: yup
.number()
.integer("年齢は整数で入力してください")
.min(18, "18歳以上である必要があります")
.max(120, "有効な年齢を入力してください")
.optional(),
role: yup
.string()
.required("ロールは必須です")
.oneOf(["admin", "user"] as const, "admin または user を選択してください"),
password: yup
.string()
.required("パスワードは必須です")
.min(8, "パスワードは8文字以上で入力してください")
.matches(
/^(?=.*[a-zA-Z])(?=.*\d)/,
"パスワードは英字と数字の両方を含めてください"
),
});
// 型推論(Zodほどの精度ではない)
type User = yup.InferType<typeof userSchema>;
// => role は string 型になる("admin" | "user" のリテラル型にならない場合がある)
// バリデーション実行(非同期)
async function validateUser() {
try {
const validData = await userSchema.validate(
{
name: "田中太郎",
email: "tanaka@example.com",
age: 30,
role: "admin",
password: "securePass1",
},
{ abortEarly: false } // すべてのエラーを収集
);
console.log("有効なデータ:", validData);
} catch (err) {
if (err instanceof yup.ValidationError) {
console.error("バリデーションエラー:", err.errors);
}
}
}
validateUser();
コード例から見える違い
| 観点 | Zod | Yup |
|---|---|---|
| 必須/任意の扱い | デフォルトが必須(.optional() で任意に) | .required() を明示的に付ける |
| バリデーション実行 | 同期(safeParse)/ 非同期(safeParseAsync)を選択可能 | デフォルトが非同期(validateSync で同期も可能) |
| エラーハンドリング | Result 型パターン(success / error) | try-catch パターン |
| enum の型推論 | ✅ リテラルユニオン型として推論 | ⚠️ string に広がる場合がある |
5. どちらを選ぶべきか
✅ Zodを選ぶべきケース
- 新規のTypeScriptプロジェクト — 型推論の恩恵を最大限に受けられます
- tRPC / Next.js Server Actions を使う場合 — ファーストクラスサポートがあります
- APIのリクエスト/レスポンスバリデーション — フロントエンドとバックエンドでスキーマを共有できます
- 型定義の二重管理を排除したい場合 —
z.inferで型が自動生成されます - React Hook Form を使う場合 —
@hookform/resolvers/zodで簡潔に統合できます
✅ Yupを選ぶべきケース
- 既存のFormikベースのプロジェクト — 移行コストをかけずにそのまま使えます
- JavaScriptプロジェクト(型推論が不要) — Yupの直感的なAPIが活きます
- チームにTypeScript経験が浅いメンバーがいる場合 — 学習コストが低いです
- 既にYupで大量のスキーマが定義されている場合 — 動いているものを無理に移行する必要はありません
⚠️ 移行を検討すべきタイミング
既存のYupプロジェクトでも、以下の状況ではZodへの移行を検討する価値があります。
- 型定義とバリデーションスキーマの乖離によるバグが頻発している
- tRPCなどZod前提のライブラリを導入したい
- TypeScriptの
strictモードを有効にして型安全性を高めたい
6. まとめ
ZodとYupはどちらも成熟したバリデーションライブラリですが、設計思想が異なります。
| Zod | Yup | |
|---|---|---|
| 設計思想 | TypeScript-first | JavaScript-first(型は後付け) |
| 向いている時代 | 2020年代のモダンスタック | 2010年代後半〜のReactエコシステム |
| 一言で表すと | 「型とバリデーションを統一する」 | 「シンプルにバリデーションする」 |
2025年現在、TypeScriptの採用率は年々上昇しており、エコシステム全体がZodを中心に動き始めています。新規プロジェクトであれば Zodを標準選択 とし、既存プロジェクトでは 無理に移行せずYupを使い続ける という判断が、最も実務的なアプローチです。
どちらを選んでも「バリデーションをスキーマとして宣言的に書く」という本質は同じです。プロジェクトの状況とチームのスキルセットに合わせて、最適な選択をしてください。