🙌
【TS/JS】 Zodライブラリで始めるデータ検証とスキーマ宣言まとめ
Zodは、TypeScript/JavaScript環境でデータの検証を行い、スキーマ宣言を通じて型安全性を強化するライブラリです。
Zodの役割
-
ランタイムデータ検証
- APIリクエストボディ、URLパラメータ、環境変数など、外部から受け取るデータは信頼できない
- Zodを利用することで、予期しない値(型不一致・必須値の欠落など)を事前に排除できる
-
型の自動生成
- ZodスキーマからTypeScriptの型を推論可能
- 型宣言と検証ロジックを別々に記述する必要がなくなる
-
エラーメッセージの提供
- どのフィールドが、なぜ失敗したのかを明確に把握できる
-
メンテナンスしやすい
- 「スキーマ = 型定義 + バリデーション」という形で一元管理できるので、あとから仕様が変わっても修正がしやすい
設置
npm install zod # npm
yarn add zod # yarn
基本文法
import { z } from "zod";
const UserSchema = z.object({
// ① まず基本の型を指定
name: z.string(),
// ② その型に対してチェーンで条件を追加
age: z.number().int().positive(), // 整数 & 正の数
// ③ emailも同様
email: z.string().email(),
});
// データ検証
const result = UserSchema.safeParse({
name: "山田太郎",
age: 25,
email: "test@example.com"
});
if (result.success) {
console.log("✅ 有効なデータ:", result.data);
} else {
console.error("❌ 検証失敗:", result.error.format());
}
簡単な例と結果
// ログイン情報の検証例
import { z } from "zod";
//エラーメッセージは第2引数に直接渡せる
const LoginSchema = z.object({
email: z.string().email("メール形式ではありません。"),
password: z.string().min(6, "パスワードは6文字以上で入力してください。"),
});
const input = {
email: "abc.com", // 不正なメール形式
password: "123" // 短すぎる
};
const result = LoginSchema.safeParse(input);
if (!result.success) {
console.log(result.error.format());
}
/* 結果 */
{
"email": { "_errors": ["メール形式ではありません。"] },
"password": { "_errors": ["パスワードは6文字以上で入力してください。"] }
}
Zod 実践チートシート(よく使う文法)
以下、チャットGPTまとめになります。
0) 基本パターン
import { z } from "zod";
// ① スキーマ宣言
const UserSchema = z.object({
name: z.string().min(1, "名前は必須です。"),
email: z.string().email("メール形式ではありません。"),
age: z.coerce.number().int().positive("正の整数で入力してください。"),
});
// ② 検証 & エラーハンドリング
const r = UserSchema.safeParse(input);
if (!r.success) return { errors: r.error.format() };
const user = r.data; // 型安全に利用可能
// ③ 型推論
type User = z.infer<typeof UserSchema>;
1) 基本型(文字列・数値・真偽値・日付)
// string
z.string({ required_error: "必須です。" })
.min(1, "1文字以上")
.max(50, "50文字以下")
.email("メール形式ではありません。")
.url("URL形式ではありません。")
.uuid("UUID形式ではありません。")
.regex(/^[a-z0-9-]+$/i, "半角英数字と-のみ");
// number
z.number({ invalid_type_error: "数値で入力してください。" })
.int("整数で入力してください。")
.positive("正の数のみ")
.gte(0, "0以上")
.lte(100, "100以下");
// boolean / date
z.boolean({ invalid_type_error: "true/falseを指定してください。" });
z.date().min(new Date("2025-01-01"), "2025-01-01以降の日付");
2) optional / nullable / default
z.string().optional(); // undefinedを許可
z.string().nullable(); // nullを許可
z.string().nullish(); // null | undefinedを許可
z.string().default("guest"); // デフォルト値
3) 配列・レコード・オブジェクト
// 配列
z.array(z.string()).nonempty("1件以上入力してください。");
// レコード型 { [key: string]: number }
z.record(z.string(), z.number());
// オブジェクトの余計なキーをどうするか
z.object({ name: z.string() }).strict(); // 定義外キーはエラー
z.object({ name: z.string() }).passthrough(); // 定義外キーも許可(デフォルト)
z.object({ name: z.string() }).strip(); // 定義外キーは削除
4) スキーマ再利用
const BaseUser = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const CreateUser = BaseUser.extend({ password: z.string().min(8) });
const UpdateUser = BaseUser.partial(); // 全てoptionalに
const PublicUser = BaseUser.omit({ email: true }); // emailを除外
const EmailOnly = BaseUser.pick({ email: true }); // emailだけ
const AdminUser = BaseUser.merge(z.object({ role: z.literal("admin") }));
5) 文字列 → 数値・真偽値(coerce)
const QuerySchema = z.object({
page: z.coerce.number().int().gte(1).default(1),
perPage: z.coerce.number().int().min(1).max(100).default(20),
published: z.coerce.boolean().default(false),
});
6) transform:検証 + 加工
// 価格(円) → セントに変換
const PriceSchema = z.coerce.number().min(0)
.transform(v => Math.round(v * 100));
// 空白を削除し、空文字は禁止
const NonEmptyTrimmed = z.string()
.transform(s => s.trim())
.refine(s => s.length > 0, { message: "空白のみは不可" });
7) refine / superRefine:ビジネスルール
// 単一フィールド
const Password = z.string().min(8)
.refine(v => /[0-9]/.test(v), { message: "数字を含めてください。" });
// 複数フィールド
const Signup = z.object({
password: z.string().min(8),
confirm : z.string().min(8),
}).superRefine((data, ctx) => {
if (data.password !== data.confirm) {
ctx.addIssue({
code: "custom",
path: ["confirm"],
message: "パスワードが一致しません。",
});
}
});
8) Union / Discriminated Union / Intersection
// Union
const Result = z.union([z.string(), z.number()]);
// 判別ユニオン
const Event = z.discriminatedUnion("type", [
z.object({ type: z.literal("created"), id: z.string() }),
z.object({ type: z.literal("deleted"), id: z.string(), reason: z.string().optional() }),
]);
// Intersection
const AB = z.intersection(
z.object({ a: z.string() }),
z.object({ b: z.number() })
);
9) Enum / nativeEnum
// Enum
const Role = z.enum(["admin", "user", "guest"]);
type Role = z.infer<typeof Role>;
// TypeScript enumと接続
enum Status { READY = "READY", DONE = "DONE" }
const StatusSchema = z.nativeEnum(Status);
10) APIルートごとのスキーマ
// Body
const Body = z.object({
name: z.string({ required_error: "名前は必須です。" }).min(1),
email: z.string().email(),
});
// Query
const Query = z.object({
page: z.coerce.number().int().gte(1).default(1),
});
// Params (/users/[id])
const Params = z.object({
id: z.string().uuid("UUID形式で指定してください。"),
});
// Headers
const Headers = z.object({
authorization: z.string().startsWith("Bearer ", "Bearerトークンが必要です。"),
});
// Env
const Env = z.object({
NODE_ENV: z.enum(["development", "test", "production"]),
DATABASE_URL: z.string().url(),
}).parse(process.env);
11) エラーハンドリングパターン
function validate<T extends z.ZodTypeAny>(schema: T, data: unknown) {
const r = schema.safeParse(data);
return r.success
? { data: r.data, errors: null }
: { data: null, errors: r.error.format() };
}
12) Next.js Route 実践例
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";
const Body = z.object({
name: z.string({ required_error: "名前は必須です。" }).min(1, "名前は必須です。"),
email: z.string().email("メール形式ではありません。"),
age: z.coerce.number().int().positive("正の整数で入力してください。").optional(),
});
export async function POST(req: NextRequest) {
const json = await req.json().catch(() => null);
const parsed = Body.safeParse(json);
if (!parsed.success) {
return NextResponse.json({ errors: parsed.error.format() }, { status: 400 });
}
const body = parsed.data;
// …DB処理など
return NextResponse.json({ ok: true });
}
✅ まとめ
-
基本型は
.min()
,.max()
,.email()
,.positive()
などでメッセージ指定 - optional / nullable / default で入力の柔軟性
- coerce で文字列 → 数値/boolean 変換(フォーム・URLで超便利)
- refine / superRefine でビジネスルールチェック
- extend / pick / omit / partial / merge でスキーマ再利用
- safeParse + error.format() が定番パターン
- z.setErrorMap を使えばプロジェクト全体のエラーメッセージを統一可能
Discussion