Zod
ライブラリ
Zod
概要
Zodは、TypeScriptファーストのスキーマ宣言とバリデーションライブラリです。静的型推論により、スキーマ定義から自動的にTypeScriptの型を生成し、ランタイムでの型検証とコンパイル時の型安全性を両立します。APIレスポンスの検証、フォームバリデーション、環境変数の型チェックなど、外部データの安全な取り込みに最適です。直感的なAPIと優れたエラーメッセージにより、開発者体験を大幅に向上させます。
詳細
Zodは、TypeScriptの型システムを最大限に活用したスキーマバリデーションライブラリです。単一のスキーマ定義から、TypeScriptの型とランタイムバリデーターの両方を生成することで、型定義の重複を排除します。関数型プログラミングの原則に基づいた設計により、スキーマの合成や変換が容易で、複雑なデータ構造にも対応できます。parse(例外をスロー)とsafeParse(結果オブジェクトを返す)の2つのAPIにより、エラーハンドリングも柔軟に行えます。
主な特徴
- TypeScript型推論: スキーマから自動的に型を生成
- 包括的な型サポート: プリミティブ、オブジェクト、配列、ユニオン、交差型など
- カスタムバリデーション: refineメソッドによる柔軟な検証ロジック
- エラーハンドリング: 詳細で分かりやすいエラーメッセージ
- 変換機能: transform、preprocessによるデータ変換
- 非同期バリデーション: 外部APIを使用した検証も可能
メリット・デメリット
メリット
- TypeScriptとの完璧な統合(型推論が強力)
- ゼロ依存で軽量(約8KB gzipped)
- 直感的で読みやすいAPI設計
- 詳細なエラー情報とカスタマイズ可能なメッセージ
- スキーマの合成と再利用が容易
- アクティブな開発とコミュニティサポート
デメリット
- ランタイムオーバーヘッドがある
- 大規模なスキーマでは初期化コストが高い
- JSON Schemaとの直接的な互換性なし
- 一部の高度な型(条件付き型など)は表現困難
- バンドルサイズが気になる場合がある
- 学習曲線がやや急(高度な機能)
参考ページ
書き方の例
基本的なスキーマ定義と検証
import { z } from 'zod';
// スキーマ定義
const UserSchema = z.object({
id: z.number().positive(),
name: z.string().min(1).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150),
isActive: z.boolean().default(true),
roles: z.array(z.string()).optional()
});
// TypeScript型の自動生成
type User = z.infer<typeof UserSchema>;
// バリデーション実行
try {
const user = UserSchema.parse({
id: 1,
name: "田中太郎",
email: "[email protected]",
age: 30
});
console.log("Valid user:", user);
} catch (error) {
if (error instanceof z.ZodError) {
console.error("Validation errors:", error.errors);
}
}
// 安全なパース
const result = UserSchema.safeParse(userData);
if (result.success) {
console.log("Valid data:", result.data);
} else {
console.error("Validation failed:", result.error.format());
}
APIレスポンスの検証
import { z } from 'zod';
// APIレスポンススキーマ
const ApiResponseSchema = z.object({
status: z.literal('success').or(z.literal('error')),
data: z.object({
users: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
createdAt: z.string().datetime()
})),
total: z.number(),
page: z.number(),
pageSize: z.number()
}).optional(),
error: z.object({
code: z.string(),
message: z.string()
}).optional()
});
// API呼び出しとバリデーション
async function fetchUsers(page: number) {
const response = await fetch(`/api/users?page=${page}`);
const json = await response.json();
// レスポンスを検証
const validatedData = ApiResponseSchema.parse(json);
if (validatedData.status === 'success' && validatedData.data) {
return validatedData.data.users;
} else if (validatedData.error) {
throw new Error(validatedData.error.message);
}
}
カスタムバリデーションと変換
import { z } from 'zod';
// カスタムバリデーション
const PasswordSchema = z
.string()
.min(8, "パスワードは8文字以上必要です")
.refine(
(password) => /[A-Z]/.test(password),
"大文字を1文字以上含む必要があります"
)
.refine(
(password) => /[0-9]/.test(password),
"数字を1文字以上含む必要があります"
)
.refine(
(password) => /[!@#$%^&*]/.test(password),
"特殊文字を1文字以上含む必要があります"
);
// データ変換
const DateSchema = z
.string()
.transform((str) => new Date(str))
.refine((date) => !isNaN(date.getTime()), {
message: "有効な日付形式ではありません"
});
// 前処理を含むスキーマ
const TrimmedString = z.preprocess(
(val) => typeof val === 'string' ? val.trim() : val,
z.string()
);
// 使用例
const password = PasswordSchema.parse("MyP@ssw0rd");
const date = DateSchema.parse("2024-01-15");
const trimmed = TrimmedString.parse(" hello world "); // "hello world"
フォームバリデーション
import { z } from 'zod';
// フォームスキーマ
const RegistrationFormSchema = z.object({
username: z
.string()
.min(3, "ユーザー名は3文字以上必要です")
.max(20, "ユーザー名は20文字以下にしてください")
.regex(/^[a-zA-Z0-9_]+$/, "英数字とアンダースコアのみ使用可能です"),
email: z
.string()
.email("有効なメールアドレスを入力してください"),
password: z
.string()
.min(8, "パスワードは8文字以上必要です"),
confirmPassword: z.string(),
age: z
.number()
.int("整数を入力してください")
.min(18, "18歳以上である必要があります"),
terms: z
.boolean()
.refine((val) => val === true, "利用規約に同意してください")
}).refine((data) => data.password === data.confirmPassword, {
message: "パスワードが一致しません",
path: ["confirmPassword"]
});
// バリデーション関数
function validateForm(formData: unknown) {
const result = RegistrationFormSchema.safeParse(formData);
if (!result.success) {
// フィールドごとのエラーを取得
const fieldErrors = result.error.flatten().fieldErrors;
return { success: false, errors: fieldErrors };
}
return { success: true, data: result.data };
}
環境変数の型安全な管理
import { z } from 'zod';
// 環境変数スキーマ
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).pipe(z.number().positive()),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
ENABLE_LOGGING: z
.string()
.transform((val) => val === 'true')
.pipe(z.boolean())
.default('false'),
MAX_CONNECTIONS: z
.string()
.optional()
.transform((val) => val ? Number(val) : 10)
.pipe(z.number().positive())
});
// 環境変数の検証
const env = EnvSchema.parse(process.env);
// 型安全なアクセス
export const config = {
nodeEnv: env.NODE_ENV,
port: env.PORT,
databaseUrl: env.DATABASE_URL,
apiKey: env.API_KEY,
enableLogging: env.ENABLE_LOGGING,
maxConnections: env.MAX_CONNECTIONS
} as const;
// 使用例
if (config.nodeEnv === 'production') {
// 本番環境の設定
}
複雑な型の合成と拡張
import { z } from 'zod';
// 基本スキーマ
const BaseProductSchema = z.object({
id: z.string().uuid(),
name: z.string(),
price: z.number().positive()
});
// 拡張スキーマ
const DetailedProductSchema = BaseProductSchema.extend({
description: z.string(),
category: z.enum(['electronics', 'clothing', 'food']),
tags: z.array(z.string()),
stock: z.number().int().nonnegative()
});
// ユニオン型
const NotificationSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('email'),
to: z.string().email(),
subject: z.string(),
body: z.string()
}),
z.object({
type: z.literal('sms'),
phoneNumber: z.string().regex(/^\+[1-9]\d{1,14}$/),
message: z.string().max(160)
}),
z.object({
type: z.literal('push'),
deviceToken: z.string(),
title: z.string(),
body: z.string(),
data: z.record(z.string()).optional()
})
]);
// 条件付きスキーマ
const PaymentSchema = z
.object({
method: z.enum(['credit_card', 'bank_transfer', 'paypal']),
amount: z.number().positive()
})
.and(
z.union([
z.object({
method: z.literal('credit_card'),
cardNumber: z.string().length(16),
cvv: z.string().length(3)
}),
z.object({
method: z.literal('bank_transfer'),
accountNumber: z.string(),
routingNumber: z.string()
}),
z.object({
method: z.literal('paypal'),
email: z.string().email()
})
])
);
// 使用例
const notification = NotificationSchema.parse({
type: 'email',
to: '[email protected]',
subject: 'Welcome!',
body: 'Thank you for signing up.'
});