Zod でスキーマバリデーション
前章では、バリデーションを if 文で 1 つずつ手書きしました。フィールドが増えるほどこのコードは膨らみ、「型」と「検証ルール」が二重管理になります。この章では、スキーマを 1 つ定義するだけで 実行時の検証 と 静的な型 の両方が手に入る Zod を学びます。
この章で学ぶこと
- Zod でスキーマを定義し、
z.inferで型を導出する - React Hook Form と組み合わせてフォームを型安全にバリデーションする
safeParseで API レスポンスなど外部データを実行時に検証する- Zod v4 で変わった API の注意点
この章では zod と、React Hook Form 連携用の @hookform/resolvers を使います。Zod は 2025 年に v4 へメジャーアップデートされており、本章は v4 を前提とします。
Zod とは
Zod は TypeScript ファーストのスキーマ宣言・検証ライブラリです。スキーマを 1 つ書くと、次の 2 つが同時に手に入ります。
- 実行時バリデーション: 値がスキーマに合うかを実行時にチェックする
- 静的な型: スキーマから TypeScript の型を導出する(手書きの
type定義が不要)
TypeScript の型は コンパイル時に消えるため、ユーザー入力や API レスポンスのような「実行時に初めて分かる値」は型だけでは守れません。Zod はこの境界を埋めます。
インストール
npm install zod
# React Hook Form と組み合わせる場合
npm install react-hook-form @hookform/resolvers
スキーマ定義の基本
z.object() でオブジェクトの形を宣言します。
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.email('メールアドレスの形式が正しくありません'),
age: z.number().int().min(0).max(120),
role: z.enum(['admin', 'member']),
})
z.string()/z.number()で基本型、.min()/.max()で制約を足すz.email()はメール形式の検証(Zod v4 ではz.string().email()から top-level のz.email()に変わりました)z.enum()で取りうる値を限定する- 各メソッドの引数にエラーメッセージを渡せる
検証は parse(失敗時に例外を投げる)または safeParse(失敗を戻り値で返す)で実行します。フォームや外部データでは、例外に頼らず分岐できる safeParse が扱いやすいです。
const result = userSchema.safeParse({
name: '太郎',
email: 'taro@example.com',
age: 30,
role: 'member',
})
if (result.success) {
console.log(result.data) // 検証済みの値(型付き)
} else {
console.log(result.error.issues) // どのフィールドがなぜ失敗したか
}
z.infer で型を導出
スキーマから z.infer で型を取り出せます。型と検証ルールを 1 か所にまとめられるのが Zod の核心です。
type User = z.infer<typeof userSchema>
// 次の型と等価:
// { name: string; email: string; age: number; role: 'admin' | 'member' }
type を手書きすると、検証ルールを変えたときに型の更新を忘れがちです。z.infer ならスキーマを直すだけで型も追従します。
React Hook Form との連携
@hookform/resolvers/zod の zodResolver を useForm に渡すと、Zod のスキーマがそのままフォームのバリデーションになります。
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const signupSchema = z.object({
email: z.email('メールアドレスの形式が正しくありません'),
password: z.string().min(8, '8 文字以上で入力してください'),
})
type SignupValues = z.infer<typeof signupSchema>
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignupValues>({
resolver: zodResolver(signupSchema),
})
const onSubmit = (data: SignupValues) => {
// data はスキーマを通過した型安全な値
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input type="email" {...register('email')} />
{errors.email && <span role="alert">{errors.email.message}</span>}
</div>
<div>
<input type="password" {...register('password')} />
{errors.password && <span role="alert">{errors.password.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>登録</button>
</form>
)
}
前章の手書きバリデーションと比べて、検証ロジックがスキーマに集約され、errors.email.message にスキーマで書いたメッセージがそのまま入る点に注目してください。
React Hook Form 自体の API(register / handleSubmit / 非制御コンポーネント)は前章のフォームライブラリ節を参照してください。本章は Zod 側に focus しています。
API レスポンスの実行時検証
Zod の価値はフォームだけではありません。fetch で受け取った JSON は、TypeScript 上では型を自己申告しているだけで、実際にその形である保証はありません。
// よくある書き方: 実行時には何も検証していない
const res = await fetch('/api/users/1')
const user = (await res.json()) as User // as は嘘をつける
API の仕様変更やバックエンドのバグで形が変わっても、as では気づけません。safeParse を通せば、実行時に形を保証できます。
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('取得に失敗しました')
const json = await res.json()
const parsed = userSchema.safeParse(json)
if (!parsed.success) {
// 期待した形でない = バックエンドとの契約違反として扱う
throw new Error('レスポンスの形式が不正です')
}
return parsed.data // ここから先は型安全
}
データフェッチング(TanStack Query)の queryFn の中で safeParse を呼べば、キャッシュに入る前の段階で外部データを検証できます。型アサーション(as)を実行時の検証に置き換えるイメージです。
Zod v4 の注意点
Zod は 2025 年に v4 へメジャーアップデートされ、一部 API が v3 から変わっています。ネット上の記事は v3 前提のものが多いため、必ず Zod 公式ドキュメント で現行の API を確認してください。
- 文字列フォーマットが top-level に:
z.string().email()→z.email()(z.url()/z.uuid()なども同様) - エラーカスタマイズの統一:
{ message: ... }が{ error: ... }に統合(messageは当面動くが非推奨) - React Hook Form 連携:
@hookform/resolvers/zodの Zod v4 対応版を使う
まとめ
- Zod はスキーマ 1 つから 実行時検証 と 静的な型(
z.infer)の両方を得る zodResolverで React Hook Form のバリデーションをスキーマに集約できるsafeParseは API レスポンスなど外部データの実行時検証にも使える- 型推論の視点でさらに深掘りしたい場合は TypeScript で React を書く も参照
次に読む
フォームとバリデーションの基礎が固まりました。次は副作用を扱う useEffect に進みます。