フォームバリデーション比較:Zod vs Yup vs Valibot でTypeScriptのバリデーションを実装する
オープンソースラボ編集部 ・ 2026年6月14日
フォームバリデーション比較:Zod vs Yup vs Valibot でTypeScriptのバリデーションを実装する
フォームバリデーション・APIリクエスト検証・設定ファイルのスキーマチェックに使われるTypeScript/JavaScriptのランタイムバリデーションライブラリは開発の基盤ツールです。Zod(TypeScript First・最大シェア)・Yup(JavaScript製・Formik連携の老舗)・Valibot(超軽量・モジュラー)の3つが2026年の主要バリデーションライブラリです。
バリデーションライブラリを使う理由
- 型安全: TypeScriptの型定義とランタイムバリデーションを一元管理(二重定義を排除)
- API境界保護: 外部からのリクエストボディをスキーマで検証してSQLインジェクション・型エラーを防止
- エラーメッセージ管理: バリデーションエラーの国際化・カスタムメッセージを宣言的に定義
- スキーマ共有: フロントエンドとバックエンドで同一スキーマを再利用してDRYを実現
主要ライブラリの概要
Zod
2020年公開、TypeScript製のOSSです。GitHubスター35k+。TypeScript First設計で最もシェアが高いバリデーションライブラリです。スキーマから自動的にTypeScript型を推論でき、React Hook Form・tRPC・Prismaとのエコシステム統合が最も充実しています。
// Zod: 包括的なスキーマ定義と型推論
import { z } from 'zod'
// 基本スキーマ定義
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, '名前は必須です').max(100, '名前は100文字以内で入力してください'),
email: z.string().email('有効なメールアドレスを入力してください'),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user', 'guest']).default('user'),
createdAt: z.date().default(() => new Date()),
metadata: z.record(z.string(), z.unknown()).optional(),
})
// スキーマから型を自動推論(型定義の二重管理が不要)
type User = z.infer<typeof UserSchema>
// バリデーション実行
const result = UserSchema.safeParse({
id: '123e4567-e89b-12d3-a456-426614174000',
name: '丸山侑太',
email: 'user@example.com',
role: 'admin',
})
if (result.success) {
console.log(result.data) // 型安全なデータ(User型)
} else {
console.error(result.error.errors) // バリデーションエラー配列
}
// 変換・前処理(.transform・.preprocess)
const NormalizedEmailSchema = z.string()
.email()
.toLowerCase() // メールアドレスを小文字に変換
.trim() // 前後の空白を削除
const CoercedDateSchema = z.coerce.date() // 文字列→Date型に自動変換
// 条件付きバリデーション(.superRefine・.refine)
const PasswordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'パスワードが一致しません',
path: ['confirmPassword'], // エラーをconfirmPasswordフィールドに紐付け
})
// discriminated union(タグ付きユニオン)
const PaymentSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('credit_card'),
cardNumber: z.string().regex(/^\d{16}$/, '16桁のカード番号を入力'),
expiryDate: z.string().regex(/^\d{2}\/\d{2}$/, 'MM/YY形式で入力'),
}),
z.object({
method: z.literal('bank_transfer'),
bankCode: z.string().regex(/^\d{4}$/),
accountNumber: z.string().regex(/^\d{7,8}$/),
}),
])
// Zod + React Hook Form の統合
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const ContactSchema = z.object({
name: z.string().min(1, '名前を入力してください'),
email: z.string().email('有効なメールアドレスを入力してください'),
message: z.string().min(10, '10文字以上入力してください').max(2000),
subscribe: z.boolean().default(false),
})
type ContactForm = z.infer<typeof ContactSchema>
export function ContactForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<ContactForm>({
resolver: zodResolver(ContactSchema),
defaultValues: { subscribe: false },
})
const onSubmit = async (data: ContactForm) => {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data),
})
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<button type="submit" disabled={isSubmitting}>送信</button>
</form>
)
}
// Next.js App Router: Server ActionでZodバリデーション
'use server'
import { z } from 'zod'
const CreateArticleSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(100),
category: z.enum(['tech', 'design', 'business']),
tags: z.array(z.string()).max(10),
})
export async function createArticle(formData: FormData) {
const raw = Object.fromEntries(formData)
const parsed = CreateArticleSchema.safeParse({
...raw,
tags: formData.getAll('tags'),
})
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
// バリデーション済みデータを使ってDB保存
const article = await db.article.create({ data: parsed.data })
return { success: true, id: article.id }
}
Valibot
2023年公開、TypeScript製のOSSです。GitHubスター7k+。Zodより最大98%小さいバンドルサイズ(8KB→1.4KB)を実現するモジュラー設計のバリデーションライブラリです。Tree-shakingが完全に機能してインポートした関数のみがバンドルに含まれます。
// Valibot: 超軽量・モジュラーバリデーション
import * as v from 'valibot'
// Zodと同等の機能をより小さなバンドルサイズで実現
const UserSchema = v.object({
name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
email: v.pipe(v.string(), v.email()),
age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
role: v.picklist(['admin', 'user', 'guest']),
})
type User = v.InferOutput<typeof UserSchema>
const result = v.safeParse(UserSchema, { name: '太郎', email: 'test@example.com', role: 'user' })
if (result.success) {
console.log(result.output)
}
Yup
2017年公開、JavaScript製のOSSです。GitHubスター22k+。Formik・React Hook Form両対応の老舗バリデーションライブラリで、.when()による条件付きバリデーションや非同期バリデーション(API呼び出し)が直感的に書けます。
// Yup: 条件付き・非同期バリデーション
import * as yup from 'yup'
const RegisterSchema = yup.object({
username: yup.string()
.required('ユーザー名は必須')
.min(3).max(20)
.test('unique', 'このユーザー名は使用済みです', async (value) => {
if (!value) return false
const res = await fetch(`/api/check-username?username=${value}`)
const { available } = await res.json()
return available
}),
isCompany: yup.boolean().default(false),
companyName: yup.string().when('isCompany', {
is: true,
then: schema => schema.required('法人名は必須です').min(1),
otherwise: schema => schema.optional(),
}),
})
機能比較表
| 比較項目 | Zod | Valibot | Yup |
|---|---|---|---|
| バンドルサイズ | 8KB | ✅ 1.4KB | 12KB |
| TypeScript推論 | ✅ | ✅ | △ |
| 非同期バリデーション | ✅ | ✅ | ✅ |
| tRPC統合 | ✅ | ✅ | △ |
| GitHub Stars | 35k+ | 7k+ | 22k+ |
バリデーションライブラリはDevOpsカテゴリ/categories/devopsのCI/CDパイプラインでAPIスキーマ変更を自動検知して型安全を保証するテストと組み合わせます。LLM Toolsカテゴリ/categories/llm-toolsのAI生成コンテンツ(構造化出力)をZodスキーマで検証してLLMの出力形式を型安全に保証するパターン(Structured Output)に広く活用されています。
FAQ
Q. ZodでLLMの構造化出力(Structured Output)を型安全に扱うには?
A. Anthropic/OpenAI の Structured Output 機能とZodを組み合わせます。Anthropic APIでのZod統合(zod-to-json-schema使用): ①const schema = zodToJsonSchema(ProductSchema)でJSON Schemaに変換②toolsパラメーターにtool定義として渡す③返却されたtool_use結果をZodで再バリデーション。OpenAI Structured Output(GPT-4o): openai.beta.chat.completions.parse()にresponse_format: {type: 'json_schema', json_schema: zodToJsonSchema(schema)}を設定→message.parsedが自動的に型安全なオブジェクトとして返却されます。LangChain: withStructuredOutput(schema)メソッドがzodスキーマを直接受け付けます。
Q. ZodスキーマをPrismaスキーマと同期させるには?
A. zod-prisma-typesまたはprisma-zod-generatorでPrismaスキーマからZodスキーマを自動生成します。①schema.prismaにgenerator zod {provider = "zod-prisma-types"}を追加②npx prisma generateでPrismaスキーマの全モデルに対応するZodスキーマがprisma/generated/zod/に自動生成③生成されたスキーマをAPIのバリデーションやフォームにインポートして使用。利点: PrismaモデルとZodスキーマが常に同期されるため、スキーマ変更時の手動更新ミスが発生しない。tRPCとの組み合わせ: tRPCのinput/outputにZodスキーマを指定してフロントエンドへの型の自動伝播を実現します。
Q. Zodでカスタムバリデーションルールを作成するには?
A. .refine()(フィールドレベル)または.superRefine()(複数フィールド横断)でカスタムロジックを追加します。.refine(): z.string().refine(val => !blockedDomains.includes(val.split('@')[1]), {message: 'このドメインは使用できません'})。.superRefine(): 複数エラーを返せる高度なバリデーション。カスタムエラーマップ: z.setErrorMap((issue, ctx) => {if (issue.code === z.ZodIssueCode.too_small) return {message: 最小${issue.minimum}文字で入力してください}; return {message: ctx.defaultError}})でプロジェクト全体のエラーメッセージを一元管理できます。
Q. バンドルサイズが重要な場合はValibotとZodどちらを選ぶべきですか?
A. バンドルサイズを最優先するエッジ環境・Client Componentが多いSPAはValibot、エコシステム統合とtRPCが重要なフルスタックはZodが向いています。サイズ比較: Zodは8KB(minified+gzipped)、Valibotは1.4KB(使用関数のみTree-shaking後)。実際の影響: Next.js App RouterではServer Componentでのみ使う場合はバンドルサイズが影響しないため、Server-sideではZodを使ってもクライアントバンドルは増えません。Edge Runtime(Cloudflare Workers・Vercel Edge Functions): バンドルサイズ制限(1MB以下)が厳しいためValibotが有利。API互換性: ValibotはZodと似たAPIで移行コストが低く、@valibot/zod-compatで段階的移行が可能です。
まとめ
| ユースケース | 推奨ツール |
|---|---|
| TypeScript・tRPC・最大エコシステム | Zod |
| Edge環境・バンドルサイズ最小化 | Valibot |
| Formik連携・非同期バリデーション | Yup |