mcpField
Z
zod-validation-utilities

zod-validation-utilities

github.com/giuseppe-trisciuoglio開発3653.8k 回インストール7 週間前に更新
リポジトリ

zod-validation-utilities について

Zod v4で再利用可能かつ型安全なバリデーションを構築する、TypeScript開発者向けスキルです。エラー処理、入力変換、React Hook Formとの連携を扱います。

主な機能

  1. 01Zod v4のスキーマ定義
  2. 02入力の強制変換と変換処理
  3. 03refineによる項目間検証
  4. 04safeParseによるエラー処理
  5. 05React Hook Formとの連携
  6. 06入力型と出力型の推論

活用シーン

  1. 01APIリクエストとレスポンスの検証
  2. 02フォームや環境変数の解析
  3. 03チーム間での検証スキーマ再利用
  4. 04React Hook Formでのフォーム検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill zod-validation-utilities

インストール後、Claude Code で/zod-validation-utilitiesを入力して読み込まれたか確認します。

同梱ファイルreferences/advanced-patterns.md

詳細

Zod バリデーションユーティリティ

概要

最小限の定型コードで再利用可能かつ型安全なバリデーションを実現する、実運用向けの Zod v4 パターンです。最新の API、予測可能なエラー処理、フォーム連携に重点を置いています。

使用する場面

  • TypeScript サービスでリクエスト/レスポンスのバリデーションスキーマを定義する
  • API、フォーム、環境変数、外部システムからの信頼できない入力を解析する
  • 型強制、変換、フィールド間のバリデーションを標準化する
  • チーム間で再利用可能なスキーマユーティリティを構築する
  • zodResolver を使用して React Hook Form と Zod を連携する

手順

  1. 厳密なオブジェクトスキーマと明示的なフィールド制約から始めます
  2. エラーメッセージには、最新の Zod v4 API と error オプションを優先します
  3. 入力型が不確かな境界では型強制(z.coerce.*)を使用します
  4. ビジネス上の不変条件は、スキーマ定義の近くにある refine/superRefine に記述します
  5. 一貫性を保つため、スキーマと推論された型(z.input/z.output)の両方をエクスポートします
  6. 重複を減らすため、ユーティリティスキーマ(メール、ID、日付、ページネーション)を再利用します

バリデーションのワークフロー

API ハンドラーまたはサービスにバリデーションを組み込む場合:

  1. 定義:境界(ハンドラー、キュー、設定ローダー)でスキーマを定義します
  2. 解析:safeParse を使ってエラーを適切に処理します
  3. 確認:result.success を確認し、失敗時と成功時で処理を分岐します
  4. 使用:成功時の処理では、型推論が完全に適用された result.data を使用します
  5. 返却:フォーマット済みのエラーを返すか、バリデーション済みのデータを使って処理を続行します

完全なパターンについては、例 7(safeParse のワークフロー)を参照してください。

例

1) 最新の Zod 4 のプリミティブとオブジェクトのエラー
import { z } from "zod";

export const UserIdSchema = z.uuid({ error: "Invalid user id" });
export const EmailSchema = z.email({ error: "Invalid email" });
export const WebsiteSchema = z.url({ error: "Invalid URL" });

export const UserProfileSchema = z.object(
  {
    id: UserIdSchema,
    email: EmailSchema,
    website: WebsiteSchema.optional(),
  },
  { error: "Invalid user profile payload" }
);
2) 型強制、前処理、変換
import { z } from "zod";

export const PaginationQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  includeArchived: z.coerce.boolean().default(false),
});

export const DateFromUnknownSchema = z.preprocess(
  (value) => (typeof value === "string" || value instanceof Date ? value : undefined),
  z.coerce.date({ error: "Invalid date" })
);

export const NormalizedEmailSchema = z
  .string()
  .trim()
  .toLowerCase()
  .email({ error: "Invalid email" })
  .transform((value) => value as Lowercase<string>);
3) 複雑なスキーマ構造
import { z } from "zod";

const TagSchema = z.string().trim().min(1).max(40);

export const ProductSchema = z.object({
  sku: z.string().min(3).max(24),
  tags: z.array(TagSchema).max(15),
  attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
  dimensions: z.tuple([z.number().positive(), z.number().positive(), z.number().positive()]),
});

export const PaymentMethodSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("card"), last4: z.string().regex(/^\d{4}$/) }),
  z.object({ type: z.literal("paypal"), email: z.email() }),
  z.object({ type: z.literal("wire"), iban: z.string().min(10) }),
]);
4) refine と superRefine
import { z } from "zod";

export const PasswordSchema = z
  .string()
  .min(12)
  .refine((v) => /[A-Z]/.test(v), { error: "Must include an uppercase letter" })
  .refine((v) => /\d/.test(v), { error: "Must include a number" });

export const RegisterSchema = z
  .object({
    email: z.email(),
    password: PasswordSchema,
    confirmPassword: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: "custom",
        path: ["confirmPassword"],
        message: "Passwords do not match",
      });
    }
  });
5) オプショナル、nullable、nullish、デフォルト値
import { z } from "zod";

export const UserPreferencesSchema = z.object({
  nickname: z.string().min(2).optional(),      // undefined allowed
  bio: z.string().max(280).nullable(),         // null allowed
  avatarUrl: z.url().nullish(),                // null or undefined allowed
  locale: z.string().default("en"),           // fallback when missing
});
6) React Hook Form との連携(zodResolver)
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const ProfileFormSchema = z.object({
  name: z.string().min(2, { error: "Name too short" }),
  email: z.email({ error: "Invalid email" }),
  age: z.coerce.number().int().min(18),
});

type ProfileFormInput = z.input<typeof ProfileFormSchema>;
type ProfileFormOutput = z.output<typeof ProfileFormSchema>;

const form = useForm<ProfileFormInput, unknown, ProfileFormOutput>({
  resolver: zodResolver(ProfileFormSchema),
  criteriaMode: "all",
});
7) safeParse によるエラー処理のワークフロー
import { z } from "zod";
import type { ZodError } from "zod";

const ResultSchema = z.object({ id: z.string(), name: z.string() });

function parseAndHandle(input: unknown) {
  const result = ResultSchema.safeParse(input);

  if (!result.success) {
    const error = result.error as ZodError;
    console.error("Validation failed:", error.errors);
    return { success: false as const, error: error.format() };
  }

  return { success: true as const, data: result.data };
}

ヒント:高度な判別可能ユニオンのパターンや複雑な React Hook Form のワークフローについては、references/advanced-patterns.md を参照してください。

ベストプラクティス

  • スキーマは境界(HTTP ハンドラー、キュー、設定ローダー)の近くに配置します
  • 復旧可能なフローでは safeParse を、即時に失敗させる実行では parse を優先します
  • 一貫性を確保するため、小さなスキーマユーティリティ(id、email、slug)を共有します
  • 変換や型強制によって実行時の形状が変わる場合は、z.input と z.output を使用します
  • preprocess の過剰な使用は避け、可能な限り明示的な z.coerce.* を優先します
  • 外部ペイロードは信頼できないものとして扱い、使用前に必ずバリデーションします

制約と警告

  • 例がインストール済みの zod のメジャーバージョンと一致していることを確認します(ここでは v4 API を使用しています)
  • Zod v4 のパターンでは、カスタムエラーに error オプションを使用することを推奨します
  • 判別可能ユニオンでは、バリアント間で安定した判別キーが必要です
  • 型強制によって上流の不正なデータが隠れる可能性があるため、上限・下限や追加の検証を慎重に設定します

コメント