zod
フォームバリデーション・APIバリデーション・ランタイム型チェックに対応した、TypeScriptファーストのスキーマバリデーションライブラリで、コンパイル時の型と静的型推論を統合して活用できます。
description の原文を見る
TypeScript-first schema validation library with static type inference for form validation, API validation, and runtime type checking with compile-time types.
SKILL.md 本文
Zod 検証スキル
サマリー
TypeScript-first スキーマ検証ライブラリで、静的型推論を備えています。スキーマを一度定義すれば、実行時検証とコンパイル時型が自動的に得られます。
いつ使うか
- 型安全なデータを伴うフォーム検証
- API リクエスト/レスポンス検証
- 環境変数検証
- TypeScript 推論を伴うランタイム型チェック
- tRPC プロシージャのインプット/アウトプット
- データベーススキーマ検証 (Drizzle, Prisma)
クイックスタート
import { z } from 'zod';
// スキーマを定義
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(18),
role: z.enum(['user', 'admin'])
});
// TypeScript型を推論
type User = z.infer<typeof UserSchema>;
// データを検証
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
}
<!-- SECTION: primitives -->
プリミティブ型
基本型
import { z } from 'zod';
// 検証付き文字列
const nameSchema = z.string()
.min(2, "短すぎます")
.max(50, "長すぎます")
.trim();
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{3}$/);
// 数値
const ageSchema = z.number()
.int("整数である必要があります")
.positive()
.min(0)
.max(120);
const priceSchema = z.number()
.positive()
.multipleOf(0.01); // 通貨の精度
// 真偽値
const isActiveSchema = z.boolean();
// 日付
const createdAtSchema = z.date()
.min(new Date('2020-01-01'))
.max(new Date());
const dateStringSchema = z.string().datetime(); // ISO 8601
const dateOnlySchema = z.string().date(); // YYYY-MM-DD
特殊型
// リテラル値
const roleSchema = z.literal('admin');
const statusSchema = z.literal('pending');
// 列挙型
const ColorEnum = z.enum(['red', 'green', 'blue']);
type Color = z.infer<typeof ColorEnum>; // 'red' | 'green' | 'blue'
const NativeEnum = z.nativeEnum(MyEnum); // TypeScript enum用
// Nullable と Optional
const optionalString = z.string().optional(); // string | undefined
const nullableString = z.string().nullable(); // string | null
const nullishString = z.string().nullish(); // string | null | undefined
// デフォルト値
const countSchema = z.number().default(0);
const settingsSchema = z.object({
theme: z.string().default('light'),
notifications: z.boolean().default(true)
});
<!-- SECTION: objects_and_arrays -->
オブジェクトと配列
オブジェクトスキーマ
// 基本的なオブジェクト
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
age: z.number().optional()
});
// ネストされたオブジェクト
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zipCode: z.string()
});
const PersonSchema = z.object({
name: z.string(),
address: AddressSchema,
contacts: z.object({
email: z.string().email(),
phone: z.string().optional()
})
});
// Strict vs Passthrough
const strictSchema = z.object({ name: z.string() }).strict();
// 未知のキーを拒否
const passthroughSchema = z.object({ name: z.string() }).passthrough();
// 未知のキーを許可
const stripSchema = z.object({ name: z.string() }).strip();
// 未知のキーを削除(デフォルト)
配列スキーマ
// シンプルな配列
const stringArray = z.array(z.string());
const numberArray = z.array(z.number()).min(1).max(10);
// オブジェクトの配列
const UsersSchema = z.array(UserSchema);
// 空でない配列
const tagSchema = z.array(z.string()).nonempty("最低1つのタグが必要");
// 固定長配列(タプル)
const coordinateSchema = z.tuple([z.number(), z.number()]);
type Coordinate = z.infer<typeof coordinateSchema>; // [number, number]
// Rest付きタプル
const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());
// [string, number, ...string[]]
レコードとマップ
// レコード(動的キーを持つオブジェクト)
const userRolesSchema = z.record(
z.string(), // キー型
z.enum(['admin', 'user', 'guest']) // 値型
);
type UserRoles = z.infer<typeof userRolesSchema>;
// { [key: string]: 'admin' | 'user' | 'guest' }
// マップ
const configMapSchema = z.map(
z.string(), // キー
z.number() // 値
);
// セット
const uniqueTagsSchema = z.set(z.string());
<!-- SECTION: type_inference -->
型推論
import { z } from 'zod';
// 出力型を推論
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number()
});
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number }
// 入力型を推論(変換前)
const TransformSchema = z.object({
date: z.string().transform(s => new Date(s))
});
type Input = z.input<typeof TransformSchema>;
// { date: string }
type Output = z.output<typeof TransformSchema>;
// { date: Date }
// 推論された型を関数で使う
function createUser(data: User): void {
// dataは型安全
}
function validateAndCreate(data: unknown): User | null {
const result = UserSchema.safeParse(data);
return result.success ? result.data : null;
}
<!-- SECTION: validation_methods -->
検証メソッド
Parse vs SafeParse
// parse() - 失敗時に例外をスロー
try {
const user = UserSchema.parse(data);
// userは型User
} catch (error) {
if (error instanceof z.ZodError) {
console.error(error.issues);
}
}
// safeParse() - 結果オブジェクトを返す
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data; // 型User
} else {
const errors = result.error.issues;
errors.forEach(err => {
console.log(`${err.path}: ${err.message}`);
});
}
// parseAsync() - 非同期リファイン用
const asyncResult = await UserSchema.parseAsync(data);
// safeParseAsync() - 安全な非同期版
const asyncSafeResult = await UserSchema.safeParseAsync(data);
部分的な検証
// スキーマに合致するかチェック(例外をスローしない)
const isValid = UserSchema.safeParse(data).success;
// カスタム型ガード
function isUser(data: unknown): data is User {
return UserSchema.safeParse(data).success;
}
if (isUser(unknownData)) {
// TypeScriptはunknownDataがUserであることを知っている
console.log(unknownData.email);
}
<!-- SECTION: schema_composition -->
スキーマの構成
拡張とマージ
// 拡張(フィールドを追加)
const BaseUserSchema = z.object({
id: z.string(),
email: z.string()
});
const AdminUserSchema = BaseUserSchema.extend({
role: z.literal('admin'),
permissions: z.array(z.string())
});
// マージ(スキーマを結合)
const NameSchema = z.object({ name: z.string() });
const AgeSchema = z.object({ age: z.number() });
const PersonSchema = NameSchema.merge(AgeSchema);
// { name: string; age: number }
// ピック(フィールドを選択)
const UserIdEmail = UserSchema.pick({ id: true, email: true });
// オミット(フィールドを除外)
const UserWithoutId = UserSchema.omit({ id: true });
// Partial(全フィールドをオプション化)
const PartialUser = UserSchema.partial();
// DeepPartial(再帰的にpartial化)
const DeepPartialUser = UserSchema.deepPartial();
// Required(全フィールドを必須化)
const RequiredUser = UserSchema.required();
Union と Intersection
// Union (OR)
const StringOrNumber = z.union([z.string(), z.number()]);
// 短縮形
const StringOrNumberAlt = z.string().or(z.number());
// Discriminated Union (タグ付きunion)
const SuccessResponse = z.object({
status: z.literal('success'),
data: z.any()
});
const ErrorResponse = z.object({
status: z.literal('error'),
message: z.string()
});
const ApiResponse = z.discriminatedUnion('status', [
SuccessResponse,
ErrorResponse
]);
// Intersection (AND)
const User = z.object({ name: z.string() });
const Timestamps = z.object({
createdAt: z.date(),
updatedAt: z.date()
});
const UserWithTimestamps = z.intersection(User, Timestamps);
// 短縮形
const UserWithTimestampsAlt = User.and(Timestamps);
<!-- SECTION: transformations -->
変換とリファイン
Transform
// 検証後にデータを変換
const StringToNumber = z.string().transform(val => parseInt(val, 10));
const DateSchema = z.string().transform(str => new Date(str));
// 変換をチェーン
const TrimmedLowercase = z.string()
.transform(s => s.trim())
.transform(s => s.toLowerCase());
// 検証付きの変換
const PositiveStringNumber = z.string()
.transform(val => parseInt(val, 10))
.refine(n => n > 0, "正の数である必要があります");
// 複雑な変換
const UserInputSchema = z.object({
name: z.string().transform(s => s.trim()),
email: z.string().email().transform(s => s.toLowerCase()),
birthDate: z.string().transform(s => new Date(s)),
tags: z.string().transform(s => s.split(',').map(t => t.trim()))
});
type UserInput = z.input<typeof UserInputSchema>;
// { name: string; email: string; birthDate: string; tags: string }
type User = z.output<typeof UserInputSchema>;
// { name: string; email: string; birthDate: Date; tags: string[] }
Refine (カスタム検証)
// シンプルなリファイン
const PasswordSchema = z.string()
.min(8)
.refine(
val => /[A-Z]/.test(val),
"大文字を含む必要があります"
)
.refine(
val => /[0-9]/.test(val),
"数字を含む必要があります"
);
// カスタムエラー付きリファイン
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: "メールアドレスは既に使用されています" }
);
// オブジェクトレベルのリファイン
const PasswordMatchSchema = z.object({
password: z.string(),
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{
message: "パスワードが一致しません",
path: ["confirmPassword"] // エラー位置
}
);
// 複数フィールド検証
const DateRangeSchema = z.object({
startDate: z.date(),
endDate: z.date()
}).refine(
data => data.endDate > data.startDate,
{
message: "終了日は開始日より後である必要があります",
path: ["endDate"]
}
);
SuperRefine (高度な検証)
// 複雑な検証のためのZodコンテキストへのアクセス
const ComplexSchema = z.object({
type: z.enum(['email', 'phone']),
value: z.string()
}).superRefine((data, ctx) => {
if (data.type === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "無効なメールアドレス形式",
path: ["value"]
});
}
} else if (data.type === 'phone') {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "無効な電話番号形式",
path: ["value"]
});
}
}
});
// 複数のissue
const RegistrationSchema = z.object({
username: z.string(),
email: z.string(),
age: z.number()
}).superRefine(async (data, ctx) => {
// ユーザー名の可用性をチェック
if (await usernameTaken(data.username)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "ユーザー名は既に使用されています",
path: ["username"]
});
}
// メールアドレスの可用性をチェック
if (await emailTaken(data.email)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "メールアドレスは既に登録されています",
path: ["email"]
});
}
// 年齢制限
if (data.age < 18) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "18歳以上である必要があります",
path: ["age"]
});
}
});
<!-- SECTION: error_handling -->
エラーハンドリング
カスタムエラーメッセージ
// フィールドレベルのメッセージ
const UserSchema = z.object({
email: z.string().email({ message: "無効なメールアドレス" }),
age: z.number({
required_error: "年齢は必須です",
invalid_type_error: "年齢は数値である必要があります"
}).min(18, { message: "18歳以上である必要があります" })
});
// グローバルエラーマップ
import { z } from 'zod';
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "このフィールドはテキストである必要があります" };
}
}
if (issue.code === z.ZodIssueCode.too_small) {
if (issue.type === "string") {
return { message: `最低${issue.minimum}文字必要です` };
}
}
return { message: ctx.defaultError };
};
z.setErrorMap(customErrorMap);
エラー処理
// フォーム用にエラーをフラット化
const result = UserSchema.safeParse(data);
if (!result.success) {
const flatErrors = result.error.flatten();
console.log(flatErrors.formErrors); // トップレベルのエラー
console.log(flatErrors.fieldErrors);
// { email: ["無効なメール"], age: ["18歳以上である必要があります"] }
}
// API レスポンス用にフォーマット
function formatZodError(error: z.ZodError) {
return error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}));
}
// 使用例
const result = UserSchema.safeParse(data);
if (!result.success) {
return res.status(400).json({
errors: formatZodError(result.error)
});
}
<!-- SECTION: async_validation -->
非同期検証
import { z } from 'zod';
// 非同期リファイン
const UsernameSchema = z.string().refine(
async (username) => {
const available = await checkUsernameAvailable(username);
return available;
},
{ message: "ユーザー名は既に使用されています" }
);
// parseAsync または safeParseAsync を使用する必要があります
const result = await UsernameSchema.safeParseAsync("john_doe");
// 複雑な非同期検証
const RegistrationSchema = z.object({
username: z.string().refine(
async (val) => !(await usernameTaken(val)),
"ユーザー名は既に使用されています"
),
email: z.string().email().refine(
async (val) => !(await emailTaken(val)),
"メールアドレスは既に登録されています"
),
inviteCode: z.string().refine(
async (code) => await validateInviteCode(code),
"無効なインビットコード"
)
});
// 検証
const userData = await RegistrationSchema.parseAsync(input);
// エラーハンドリング付き
const result = await RegistrationSchema.safeParseAsync(input);
if (!result.success) {
// 検証エラーを処理
}
<!-- SECTION: advanced_types -->
高度な型
再帰的な型
// 自己参照スキーマ
type Category = {
name: string;
subcategories: Category[];
};
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema)
})
);
// ツリー構造
type TreeNode = {
value: number;
left?: TreeNode;
right?: TreeNode;
};
const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
z.object({
value: z.number(),
left: TreeNodeSchema.optional(),
right: TreeNodeSchema.optional()
})
);
Discriminated Union
// discriminatorフィールドに基づいた型安全な union
const Circle = z.object({
kind: z.literal('circle'),
radius: z.number()
});
const Rectangle = z.object({
kind: z.literal('rectangle'),
width: z.number(),
height: z.number()
});
const Triangle = z.object({
kind: z.literal('triangle'),
base: z.number(),
height: z.number()
});
const Shape = z.discriminatedUnion('kind', [
Circle,
Rectangle,
Triangle
]);
type Shape = z.infer<typeof Shape>;
// TypeScript は discriminator に基づいて型を絞り込める
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
Preprocess
// 検証前にデータを変換
const NumberFromString = z.preprocess(
(val) => (typeof val === 'string' ? parseInt(val, 10) : val),
z.number()
);
// 検証前にデータをクリーン
const TrimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
);
// JSON文字列をパース
const JsonSchema = z.preprocess(
(val) => (typeof val === 'string' ? JSON.parse(val) : val),
z.object({
name: z.string(),
age: z.number()
})
);
// フォームデータの前処理
const FormDataSchema = z.preprocess(
(data) => {
// FormDataをオブジェクトに変換
if (data instanceof FormData) {
return Object.fromEntries(data.entries());
}
return data;
},
z.object({
name: z.string(),
email: z.string().email()
})
);
Branded Types
// 名義的な型を作成
const UserId = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof UserId>;
const Email = z.string().email().brand<'Email'>();
type Email = z.infer<typeof Email>;
// 似た型の混同を防ぐ
function getUserById(id: UserId) { /* ... */ }
function sendEmail(to: Email) { /* ... */ }
const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const email = Email.parse('user@example.com');
getUserById(userId); // ✓
getUserById(email); // ✗ 型エラー
<!-- SECTION: integrations -->
インテグレーション
React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
username: z.string().min(3, "最低3文字"),
email: z.string().email("無効なメール"),
age: z.number().min(18, "18歳以上である必要があります")
});
type FormData = z.infer<typeof FormSchema>;
function MyForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(FormSchema)
});
const onSubmit = (data: FormData) => {
// dataは検証され型安全
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} />
{errors.username && <span>{errors.username.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">送信</button>
</form>
);
}
tRPC
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
// インプット/アウトプット検証
const appRouter = router({
userById: publicProcedure
.input(z.object({
id: z.string().uuid()
}))
.output(z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
}))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id }
});
return user; // アウトプットスキーマに対して型チェック
}),
createUser: publicProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18)
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
})
});
export type AppRouter = typeof appRouter;
Next.js API Routes
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional()
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validatedData = CreateUserSchema.parse(body);
// validatedDataは検証され型付き
const user = await createUser(validatedData);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.flatten().fieldErrors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: '内部サーバーエラー' },
{ status: 500 }
);
}
}
// クエリパラメータの検証
const SearchParamsSchema = z.object({
page: z.string().transform(Number).pipe(z.number().min(1)).default('1'),
limit: z.string().transform(Number).pipe(z.number().max(100)).default('10'),
sort: z.enum(['asc', 'desc']).default('asc')
});
export async function GET(request: NextRequest) {
const searchParams = Object.fromEntries(
request.nextUrl.searchParams.entries()
);
const params = SearchParamsSchema.parse(searchParams);
// params は { page: number, limit: number, sort: 'asc' | 'desc' }
const users = await getUsers(params);
return NextResponse.json(users);
}
Express ミドルウェア
import express from 'express';
import { z } from 'zod';
// 検証ミドルウェア
const validate = (schema: z.ZodSchema) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
errors: error.flatten().fieldErrors
});
}
next(error);
}
};
};
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18)
});
app.post('/users', validate(CreateUserSchema), async (req, res) => {
// req.bodyは検証済み(Express では型付きではない)
const user = await createUser(req.body);
res.json(user);
});
// params, query, body を検証
const validateRequest = (schema: {
params?: z.ZodSchema;
query?: z.ZodSchema;
body?: z.ZodSchema;
}) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
if (schema.params) {
req.params = schema.params.parse(req.params);
}
if (schema.query) {
req.query = schema.query.parse(req.query);
}
if (schema.body) {
req.body = schema.body.parse(req.body);
}
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.issues });
}
next(error);
}
};
};
app.get(
'/users/:id',
validateRequest({
params: z.object({ id: z.string().uuid() }),
query: z.object({ include: z.string().optional() })
}),
async (req, res) => {
// 検証済みの params と query
}
);
Drizzle ORM
import { z } from 'zod';
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
// テーブルを定義
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
age: integer('age')
});
// スキーマを自動生成
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
// 検証をカスタマイズ
export const customInsertUserSchema = createInsertSchema(users, {
email: z.string().email(),
age: z.number().min(18).optional()
});
// アプリケーションで使う
type NewUser = z.infer<typeof insertUserSchema>;
type User = z.infer<typeof selectUserSchema>;
function createUser(data: unknown) {
const validatedData = insertUserSchema.parse(data);
return db.insert(users).values(validatedData);
}
環境変数
// env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.string().transform(Number).pipe(z.number().min(1024)),
REDIS_HOST: z.string().default('localhost'),
REDIS_PORT: z.string().transform(Number).default('6379'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});
// 起動時に検証
export const env = envSchema.parse(process.env);
// 型安全な環境変数
export type Env = z.infer<typeof envSchema>;
// 使用例
console.log(`Server running on port ${env.PORT}`);
// env.PORT は string ではなく number
<!-- SECTION: best_practices -->
ベストプラクティス
スキーマの整理
// schemas/user.schema.ts
import { z } from 'zod';
// 再利用可能なプリミティブ
export const emailSchema = z.string().email();
export const uuidSchema = z.string().uuid();
export const passwordSchema = z.string()
.min(8)
.regex(/[A-Z]/, "大文字を含む必要があります")
.regex(/[0-9]/, "数字を含む必要があります");
// 基本スキーマ
export const baseUserSchema = z.object({
id: uuidSchema,
email: emailSchema,
name: z.string().min(2)
});
// 拡張スキーマ
export const createUserSchema = baseUserSchema.omit({ id: true }).extend({
password: passwordSchema,
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{ message: "パスワードが一致しません", path: ["confirmPassword"] }
);
export const updateUserSchema = baseUserSchema.partial().omit({ id: true });
// 型をエクスポート
export type User = z.infer<typeof baseUserSchema>;
export type CreateUser = z.infer<typeof createUserSchema>;
export type UpdateUser = z.infer<typeof updateUserSchema>;
パフォーマンス最適化
// パースされたスキーマをキャッシュ
const userSchemaCache = new Map<string, z.ZodSchema>();
function getCachedSchema(key: string, factory: () => z.ZodSchema) {
if (!userSchemaCache.has(key)) {
userSchemaCache.set(key, factory());
}
return userSchemaCache.get(key)!;
}
// 大きなオブジェクト用の遅延検証
const lazyUserSchema = z.lazy(() => z.object({
// アクセス時にのみ検証
profile: complexProfileSchema,
settings: complexSettingsSchema
}));
// 配列のストリーミング検証
async function validateLargeArray(items: unknown[]) {
const errors: z.ZodError[] = [];
for (const item of items) {
const result = ItemSchema.safeParse(item);
if (!result.success) {
errors.push(result.error);
}
}
return errors;
}
スキーマのテスト
import { describe, it, expect } from 'vitest';
describe('UserSchema', () => {
it('正しいユーザーデータを検証する', () => {
const validUser = {
email: 'user@example.com',
name: 'John Doe',
age: 25
};
expect(() => UserSchema.parse(validUser)).not.toThrow();
});
it('無効なメールアドレスを拒否する', () => {
const invalidUser = {
email: 'not-an-email',
name: 'John',
age: 25
};
const result = UserSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['email']);
}
});
it('変換を正しく適用する', () => {
const input = {
name: ' JOHN DOE ',
email: 'USER@EXAMPLE.COM'
};
const result = UserSchema.parse(input);
expect(result.name).toBe('john doe');
expect(result.email).toBe('user@example.com');
});
});
一般的なパターン
// 条件付き検証
const ConditionalSchema = z.object({
type: z.enum(['personal', 'business']),
data: z.any()
}).transform((val) => {
if (val.type === 'personal') {
return {
type: val.type,
data: PersonalDataSchema.parse(val.data)
};
} else {
return {
type: val.type,
data: BusinessDataSchema.parse(val.data)
};
}
});
// ページネーションスキーマ
export const paginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
sort: z.string().optional(),
order: z.enum(['asc', 'desc']).default('asc')
});
// フィルタースキーマ
export const filterSchema = z.object({
search: z.string().optional(),
status: z.enum(['active', 'inactive', 'pending']).optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional()
});
// API レスポンスラッパー
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
success: z.boolean(),
data: dataSchema.optional(),
error: z.string().optional(),
timestamp: z.string().datetime()
});
const userResponseSchema = apiResponseSchema(UserSchema);
Yup/Joi からのマイグレーション
// Yup -> Zod
// Yup
const yupSchema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18).required()
});
// Zod相当
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// Joi -> Zod
// Joi
const joiSchema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().min(18).required()
});
// Zod相当(上と同じ)
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// 主な違い:
// 1. Zod のフィールドはデフォルトで必須
// 2. Zod には一流の TypeScript 統合がある
// 3. Zod スキーマはイミュータブル
// 4. Zod はより良いツリーシェイキングを実現
その他のリソース
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- bobmatnyc
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/bobmatnyc/claude-mpm-skills / ライセンス: MIT
関連スキル
agent-browser
AI エージェント向けのブラウザ自動化 CLI です。ウェブサイトとの対話が必要な場合に使用します。ページ遷移、フォーム入力、ボタンクリック、スクリーンショット取得、データ抽出、ウェブアプリのテスト、ブラウザ操作の自動化など、あらゆるブラウザタスクに対応できます。「ウェブサイトを開く」「フォームに記入する」「ボタンをクリックする」「スクリーンショットを取得する」「ページからデータを抽出する」「このウェブアプリをテストする」「サイトにログインする」「ブラウザ操作を自動化する」といった要求や、プログラマティックなウェブ操作が必要なタスクで起動します。
anyskill
AnySkill — あなたのプライベート・スキルクラウド。GitHubを基盤としたリポジトリからエージェントスキルを管理、同期、動的にロードできます。自然言語でクラウドスキルを検索し、オンデマンドでプロンプトを自動ロード、カスタムスキルのアップロードと共有、スキルバンドルの一括インストールが可能です。OpenClaw、Antigravity、Claude Code、Cursorに対応しています。
engram
AIエージェント向けの永続的なメモリシステムです。バグ修正、意思決定、発見、設定変更の後はmem_saveを使用してください。ユーザーが「覚えている」「記憶している」と言及した場合、または以前のセッションと重複する作業を開始する際はmem_searchを使用します。セッション終了前にmem_session_summaryを使用して、コンテキストを保持してください。
skyvern
AI駆動のブラウザ自動化により、任意のウェブサイトを自動化できます。フォーム入力、データ抽出、ファイルダウンロード、ログイン、複数ステップのワークフロー実行など、ユーザーがウェブサイトと連携する必要があるときに使用します。Skyvernは、LLMとコンピュータビジョンを活用して、未知のサイトも自動操作可能です。Python SDK、TypeScript SDK、REST API、MCPサーバー、またはCLIを通じて統合できます。
pinchbench
PinchBenchベンチマークを実行して、OpenClawエージェントの実世界タスクにおけるパフォーマンスを評価できます。モデルの機能テスト、モデル間の比較、ベンチマーク結果のリーダーボード提出、またはOpenClawのセットアップがカレンダー、メール、リサーチ、コーディング、複数ステップのワークフローにどの程度対応しているかを確認する際に使用します。
openui
OpenUIとOpenUI Langを使用してジェネレーティブUIアプリを構築できます。これらはLLM生成インターフェースのためのトークン効率的なオープン標準です。OpenUI、@openuidev、ジェネレーティブUI、LLMからのストリーミングUI、AI向けコンポーネントライブラリ、またはjson-render/A2UIの置き換えについて述べる際に使用します。スキャフォルディング、defineComponent、システムプロンプト、Renderer、およびOpenUI Lang出力のデバッグに対応しています。