今回やること
Zodは、実行時に値を検証するためのTypeScript向けライブラリです。
TypeScriptの型は実行時のJSONを保証しません。フォーム入力やAPIリクエストの境界では、Zodのような実行時バリデーションが必要です。
この記事では次を学びます。
- Zodの基本
- スキーマ定義
- バリデーション
- 型推論
- React Hook Formとの統合
Step 1: セットアップ
npm install zod
ポイント: ZodはスキーマからTypeScriptの型を自動推論できます。z.infer<typeof schema>で型定義の二重管理が不要になります。
Step 2: 基本的なスキーマ
import { z } from 'zod';
// プリミティブ型
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
// オブジェクト
const userSchema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('有効なメールアドレスを入力してください'),
age: z.number().int().positive().optional(),
});
// 型を推論
type User = z.infer<typeof userSchema>;
Step 3: バリデーション
// parse - 失敗時に例外
try {
const user = userSchema.parse({
name: 'Alice',
email: 'alice@example.com'
});
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.issues);
}
}
// safeParse - 失敗時もオブジェクトを返す
const result = userSchema.safeParse(data);
if (result.success) {
console.log(result.data);
} else {
console.log(result.error.flatten());
}
Step 4: 高度なスキーマ
// 配列
const tagsSchema = z.array(z.string()).min(1).max(5);
// ユニオン
const statusSchema = z.enum(['active', 'inactive', 'pending']);
// 変換
const numberStringSchema = z.string().transform(val => parseInt(val, 10));
// リファインメント
const passwordSchema = z.string()
.min(8, '8文字以上必要です')
.refine(val => /[A-Z]/.test(val), '大文字を含めてください')
.refine(val => /[0-9]/.test(val), '数字を含めてください');
// 条件付きバリデーション
const formSchema = z.object({
password: z.string(),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: 'パスワードが一致しません',
path: ['confirmPassword']
});
実践メモ: `@hookform/resolvers`を使えば、ZodスキーマをReact Hook Formのバリデーターとして利用できます。サーバー側でも同じ制約を検証してください。
Step 5: React Hook Formとの統合
npm install react-hook-form @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('有効なメールアドレスを入力してください'),
});
type FormData = z.infer<typeof schema>;
function ContactForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema)
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}}
<button type="submit">送信</button>
</form>
);
}
Step 6: APIバリデーション
// Next.js API Route
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
export async function POST(request: Request) {
const body = await request.json();
const result = createUserSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ errors: result.error.flatten() },
{ status: 400 }
);
}
// result.data は型安全
const user = await createUser(result.data);
return Response.json(user, { status: 201 });
}
バリデーションを置く場所
Zodは、外部から入ってくる値を安全に扱うための境界で特に効果を発揮します。フォーム入力、APIリクエスト、環境変数、外部APIレスポンスなど、信用できない値を受け取る場所で使います。
TypeScriptの型はコンパイル時の保証ですが、実行時に届くJSONの中身までは保証しません。Zodで実行時チェックを行うことで、想定外の値を早い段階で止められます。
実装時の観点
- 入力用と出力用のスキーマを分けているか
- エラーメッセージをユーザー向けに変換しているか
- optionalとnullableの違いを意識しているか
- 環境変数も起動時に検証しているか
動作確認
Zodを導入したら、成功ケースだけでなく失敗ケースを確認します。
| 確認項目 | 入力例 | 期待する結果 |
|---|---|---|
| 正常なemail | test@example.com | 検証成功 |
| 不正なemail | abc | emailエラー |
| 短すぎる名前 | a | 文字数エラー |
| 未指定 | {} | 必須エラー |
safeParse を使うと、成功時と失敗時を result.success で分けられます。
練習
次の問い合わせフォーム用スキーマを作ってください。
nameは2文字以上emailはメールアドレス形式messageは10文字以上500文字以内safeParseで検証する- 失敗時に
error.flatten()の内容を画面表示用に変換する
次のステップ
APIで使う場合は Hono実践ガイド や tRPC実践ガイド と組み合わせると理解しやすくなります。HTTPの失敗調査は APIエラーをHTTPから切り分ける練習 を確認してください。
まとめ
注意: .parse()はバリデーション失敗時に例外をスローします。エラーハンドリングが必要な場合は.safeParse()を使いましょう。
Zodは型推論とバリデーションを統合し、TypeScriptプロジェクトで型安全な入力検証を実現します。