Zod v4リリース - TypeScript型安全バリデーションの進化

2025.07.10

公式ドキュメント

Zod v4の概要

Zod v4は、TypeScript向けスキーマバリデーションライブラリのメジャーバージョンです。パフォーマンス改善、より良いエラーメッセージ、新しいユーティリティが追加されています。

パフォーマンス改善

項目Zod v3Zod v4改善率
オブジェクトパース速度10.2μs6.8μs33% faster
配列パース速度(1000要素)850μs520μs39% faster
バンドルサイズ(gzip)14KB10KB29% smaller

新機能

  • 改善されたエラーメッセージ
  • JSON Schema出力の強化
  • メタデータAPI
  • 新しいプリミティブ型
  • カスタムバリデーションの簡素化

基本的な使い方

import { z } from 'zod';

// スキーマ定義
const UserSchema = z.object({
 id: z.string().uuid(),
 email: z.string().email(),
 name: z.string().min(2).max(100),
 age: z.number().int().positive().optional(),
 role: z.enum(['admin', 'user', 'guest']),
 preferences: z.object({
 theme: z.enum(['light', 'dark']).default('light'),
 notifications: z.boolean().default(true),
 }).optional(),
 createdAt: z.coerce.date(),
});

// 型推論
type User = z.infer<typeof UserSchema>;
// {
// id: string;
// email: string;
// name: string;
// age?: number;
// role: 'admin' | 'user' | 'guest';
// preferences?: { theme: 'light' | 'dark'; notifications: boolean };
// createdAt: Date;
// }

// バリデーション
const result = UserSchema.safeParse(input);
if (result.success) {
 console.log(result.data); // 型安全
} else {
 console.log(result.error.issues);
}

改善されたエラーメッセージ

// Zod v4: より詳細なエラーパス
const OrderSchema = z.object({
 items: z.array(
 z.object({
 productId: z.string().uuid(),
 quantity: z.number().int().positive(),
 })
 ).min(1),
});

const result = OrderSchema.safeParse({
 items: [
 { productId: 'invalid-uuid', quantity: -1 },
 ],
});

// エラー出力
result.error?.issues.forEach((issue) => {
 console.log({
 path: issue.path.join('.'), // "items.0.productId"
 message: issue.message, // "Invalid uuid"
 code: issue.code, // "invalid_string"
 });
});

カスタムエラーメッセージ

const FormSchema = z.object({
 email: z.string({
 required_error: 'メールアドレスは必須です',
 invalid_type_error: 'メールアドレスは文字列で入力してください',
 }).email({
 message: '有効なメールアドレスを入力してください',
 }),

 password: z.string()
 .min(8, { message: 'パスワードは8文字以上必要です' })
 .regex(/[A-Z]/, { message: '大文字を1文字以上含めてください' })
 .regex(/[0-9]/, { message: '数字を1文字以上含めてください' }),

 age: z.number({
 required_error: '年齢を入力してください',
 invalid_type_error: '年齢は数値で入力してください',
 }).int().min(0).max(150),
});

// 国際化対応エラーマップ
const errorMap: z.ZodErrorMap = (issue, ctx) => {
 if (issue.code === z.ZodIssueCode.too_small) {
 if (issue.type === 'string') {
 return { message: `${issue.minimum}文字以上入力してください` };
 }
 if (issue.type === 'number') {
 return { message: `${issue.minimum}以上の値を入力してください` };
 }
 }
 return { message: ctx.defaultError };
};

z.setErrorMap(errorMap);

ポイント: Zod v4のエラーメッセージはネストされたオブジェクトのパスを正確に表示し、どのフィールドでバリデーションが失敗したか一目でわかります

新しいユーティリティ型

z.coerce(型強制)

// 文字列を数値に変換
const NumberSchema = z.coerce.number();
NumberSchema.parse('42'); // 42
NumberSchema.parse('3.14'); // 3.14
NumberSchema.parse(true); // 1

// 文字列を日付に変換
const DateSchema = z.coerce.date();
DateSchema.parse('2024-01-01'); // Date
DateSchema.parse(1704067200000); // Date
DateSchema.parse(new Date()); // Date

// 文字列をブール値に変換
const BoolSchema = z.coerce.boolean();
BoolSchema.parse('true'); // true
BoolSchema.parse('false'); // false
BoolSchema.parse(1); // true
BoolSchema.parse(0); // false

z.pipe(変換チェーン)

// 文字列 → 数値 → バリデーション
const PriceSchema = z
 .string()
 .pipe(z.coerce.number().positive());

PriceSchema.parse('100'); // 100
PriceSchema.parse('-50'); // Error: Number must be positive

// JSON文字列のパース
const JsonDataSchema = z
 .string()
 .transform((str) => JSON.parse(str))
 .pipe(z.object({
 name: z.string(),
 value: z.number(),
 }));

JsonDataSchema.parse('{"name": "test", "value": 42}');

z.discriminatedUnion(判別共用体)

// イベントタイプの判別
const EventSchema = z.discriminatedUnion('type', [
 z.object({
 type: z.literal('user_created'),
 userId: z.string(),
 email: z.string().email(),
 }),
 z.object({
 type: z.literal('user_updated'),
 userId: z.string(),
 changes: z.record(z.unknown()),
 }),
 z.object({
 type: z.literal('user_deleted'),
 userId: z.string(),
 deletedAt: z.date(),
 }),
]);

type Event = z.infer<typeof EventSchema>;
// TypeScriptで判別可能
function handleEvent(event: Event) {
 switch (event.type) {
 case 'user_created':
 console.log(event.email); // 型安全
 break;
 case 'user_updated':
 console.log(event.changes);
 break;
 case 'user_deleted':
 console.log(event.deletedAt);
 break;
 }
}

実践メモ: z.toJSONSchema()を使えば、ZodスキーマからJSON Schemaを自動生成でき、OpenAPI仕様との連携が容易になります。

JSON Schema出力

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const ProductSchema = z.object({
 id: z.string().uuid().describe('Product unique identifier'),
 name: z.string().min(1).max(255).describe('Product name'),
 price: z.number().positive().describe('Price in cents'),
 category: z.enum(['electronics', 'clothing', 'food']),
 tags: z.array(z.string()).optional(),
 metadata: z.record(z.unknown()).optional(),
});

const jsonSchema = zodToJsonSchema(ProductSchema, {
 name: 'Product',
 target: 'openApi3',
});

// 出力:
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "type": "object",
// "properties": {
// "id": {
// "type": "string",
// "format": "uuid",
// "description": "Product unique identifier"
// },
// "name": {
// "type": "string",
// "minLength": 1,
// "maxLength": 255,
// "description": "Product name"
// },
// ...
// },
// "required": ["id", "name", "price", "category"]
// }

メタデータAPI

// スキーマにメタデータを付与
const UserSchema = z.object({
 id: z.string().uuid(),
 email: z.string().email(),
 name: z.string(),
}).meta({
 title: 'User',
 description: 'Represents a user in the system',
 examples: [
 { id: '123e4567-e89b-12d3-a456-426614174000', email: 'user@example.com', name: 'John Doe' },
 ],
});

// メタデータの取得
const metadata = UserSchema._def.meta;
console.log(metadata.title); // 'User'
console.log(metadata.description); // 'Represents a user in the system'

// OpenAPI生成などに活用
function generateOpenApiSchema(schema: z.ZodType) {
 const meta = schema._def.meta ?? {};
 return {
 title: meta.title,
 description: meta.description,
 examples: meta.examples,
 // ...
 };
}

ブランド型

// 型レベルでの区別
const UserId = z.string().uuid().brand<'UserId'>();
const PostId = z.string().uuid().brand<'PostId'>();

type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;

// 使用例
function getUser(id: UserId): Promise<User> { ... }
function getPost(id: PostId): Promise<Post> { ... }

const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const postId = PostId.parse('987fcdeb-51a2-34d6-b789-123456789abc');

getUser(userId); // OK
getUser(postId); // TypeScriptエラー!

注意: Zod v3からv4への移行では一部APIに破壊的変更があります。特にカスタムエラーマップやrefineの挙動変更に注意してください

フォームバリデーション

// React Hook Form との統合
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const SignUpSchema = z.object({
 email: z.string().email('有効なメールアドレスを入力してください'),
 password: z.string().min(8, '8文字以上入力してください'),
 confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
 message: 'パスワードが一致しません',
 path: ['confirmPassword'],
});

type SignUpForm = z.infer<typeof SignUpSchema>;

function SignUpPage() {
 const {
 register,
 handleSubmit,
 formState: { errors },
 } = useForm<SignUpForm>({
 resolver: zodResolver(SignUpSchema),
 });

 const onSubmit = (data: SignUpForm) => {
 console.log(data);
 };

 return (
 <form onSubmit={handleSubmit(onSubmit)}>
 <input {...register('email')} />
 {errors.email && <span>{errors.email.message}}

 <input type="password" {...register('password')} />
 {errors.password && <span>{errors.password.message}}

 <input type="password" {...register('confirmPassword')} />
 {errors.confirmPassword && <span>{errors.confirmPassword.message}}

 <button type="submit">Sign Up</button>
 </form>
 );
}

高度なパターン

再帰スキーマ

// ツリー構造
interface Category {
 id: string;
 name: string;
 children: Category[];
}

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
 z.object({
 id: z.string(),
 name: z.string(),
 children: z.array(CategorySchema),
 })
);

条件付きバリデーション

const PaymentSchema = z.discriminatedUnion('method', [
 z.object({
 method: z.literal('credit_card'),
 cardNumber: z.string().regex(/^\d{16}$/),
 expiryDate: z.string().regex(/^\d{2}\/\d{2}$/),
 cvv: z.string().regex(/^\d{3,4}$/),
 }),
 z.object({
 method: z.literal('bank_transfer'),
 bankCode: z.string(),
 accountNumber: z.string(),
 }),
 z.object({
 method: z.literal('paypal'),
 email: z.string().email(),
 }),
]);

まとめ

このニュースは、今すぐ全員が対応すべき話か、学習や新規開発で前提を更新しておく話かを分けて読むことが重要です。まずは公式情報で対象バージョンと影響範囲を確認し、自分の環境に関係する部分から見直してください。

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR