mcpField
W
web-forms-zod-validation

web-forms-zod-validation

github.com/agents-inc開発26163 回インストール2 日前に更新
リポジトリ

web-forms-zod-validation について

TypeScript 開発者向けに、Zod スキーマの検証、型推論、変換、合成、v4 移行を解説するガイドです。アプリケーション境界でのデータ検証に役立ちます。

主な機能

  1. 01安全なパースとエラー整形
  2. 02z.infer による型導出
  3. 03細かな条件とフィールド間検証
  4. 04変換と入力・出力型
  5. 05スキーマ合成とユニオン
  6. 06Zod v4 移行ガイド

活用シーン

  1. 01フォーム入力を検証して項目エラーを表示
  2. 02API 応答や URL パラメーターの確認
  3. 03条件付きバリアントや複数項目ルールの定義
  4. 04文字列を数値や日付に変換

インストール

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

$npx skills add https://github.com/agents-inc/skills --skill web-forms-zod-validation

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
advanced-patterns.md
core.md
transforms.md

詳細

Zod スキーマ検証のパターン

クイックガイド: スキーマを一度宣言し、z.infer でそこから TypeScript の型を導出すれば、ルールと型が食い違うことはありません。無効な入力が想定される場合は safeParse が結果オブジェクトを返し、バグがある場合は parse が例外をスローします。組み込みのチェックでは表現できないルールには refine と superRefine を使います。また、transform は検証中に値を変換するため、z.input と z.output が分かれます。v4 では文字列形式がトップレベルに移動し(z.email()、z.url()、z.iso.*)、v3 のメソッドチェーンは削除されたのではなく非推奨になりました。flatten()、format()、merge() には代替手段があり、移行項目の全一覧は reference.md に記載されています。

詳細リソース:

  • examples/core.md — スキーマ定義、安全な解析、エラーの整形、ユニオン、合成、非同期の追加検証
  • examples/transforms.md — 変換、強制変換、パイプチェーン、クエリパラメーター
  • examples/advanced-patterns.md — ブランド型、.catch() によるフォールバック、読み取り専用、再帰スキーマ
  • reference.md — 判断ツリー、メソッドの検索、具体的なアンチパターン、v4 移行ガイド

該当する方針

  • スキーマで値をチェックし、受け取った値をそのまま返す場合 — 必要な型ヘルパーは z.infer だけで、解析された値の形状は呼び出し側が渡した値と同じです。examples/core.md を参照してください。
  • スキーマでチェックしながら値を変換する場合 — transform、coerce、または default を使うと入力型と出力型が異なります。そのため、検証前のデータ型を受け取る関数では、パラメーターに z.input、戻り値に z.output を指定します。examples/transforms.md を参照してください。

<critical_requirements>

Zod スキーマを書く前に

無効な入力が想定される場合は、safeParse を使ってください。 例外をスローする代わりに結果オブジェクトを返すため、失敗は catch ではなく分岐として扱えます。また、result.error.issues にはフォームで必要なフィールドパスが含まれています。無効な値がバグを意味する設定や内部データには、parse を使ってください。

z.infer<typeof schema> で型を導出してください。 スキーマと並べて手書きのインターフェースを置くと、同じものを二重に宣言することになり、やがて両者に食い違いが生じます。その結果、スキーマで検証される範囲を超えた型を宣言してしまいます。

信頼できないデータが入ってくる場所で検証してください — API レスポンス、フォーム入力、設定、URL パラメーターなどです。形状の変更を境界で検出すれば、どのフィールドが変わったかがわかります。後で検出すると、数フレーム離れた場所で未定義のプロパティとして表面化します。

検証の制限値には名前を付けてください。 .min(MIN_USERNAME_LENGTH) とすれば、その数値の用途が明確になり、エラーメッセージにも同じ定数を使うことで、両者の不一致を防げます。

</critical_requirements>


自動検出: zod, z.object, z.infer, z.input, z.output, safeParse, safeParseAsync, parse, parseAsync, refine, superRefine, ctx.addIssue, transform, discriminatedUnion, z.coerce, z.pipe, z.catch, z.brand, z.lazy, z.email, z.url, z.uuid, z.iso, z.flattenError, z.treeifyError, z.strictObject, z.looseObject, ZodError

対象:

  • 信頼境界を越えるデータの検証と、失敗したフィールドの報告
  • 検証ルールからの TypeScript 型の導出
  • フィールド間のルール、条件付きの形状、判別可能なバリアント
  • 検証中の値の変換 — 文字列から数値、ISO 文字列から Date
  • 1 つのレコードの読み取り、作成、更新用のスキーマの合成

別の場所で扱うもの:

  • スキーマとフォームの接続 — フォームライブラリは独自のアダプターまたはバリデーター用スロットを通じてスキーマを受け取ります。その接続方法は、それを管理する側で決まります。
  • データの取得元 — スキーマは渡された値を検証するものであり、それ自体では I/O を行いません。
  • 永続化スキーマ — ランタイムバリデーターとテーブル定義は、同じレコードを記述していても別の成果物です。一方から他方を生成することは、そのツールの役割です。

TypeScript はコンパイルするコードをチェックし、スキーマは受け取るデータをチェックします。両者が交わるのは境界です。スキーマから型を導出するのは、誤りうるものを 1 つだけにするためです。

const UserSchema = z.object({ name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;

逆のやり方、つまりインターフェースと、それと並行して管理するスキーマでは、インターフェースにフィールドを追加してスキーマへの追加を忘れても、型チェックはどこでも通る一方で、何も検証されません。

その帰結として、スキーマを使うべきでない場面もわかります。すでに境界を越えたデータは検証済みです。コンパイラーがすでに管理している関数の中で再検証しても得るものはなく、呼び出しごとにパースのコストがかかります。


コアパターン

パターン 1:名前付きの制限値を持つスキーマ

定数をチェックとメッセージの両方で使うため、制限値を変更すれば両方に反映されます。

const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 50;

const UserSchema = z.object({
  username: z
    .string()
    .min(
      MIN_USERNAME_LENGTH,
      `Username must be at least ${MIN_USERNAME_LENGTH} characters`,
    )
    .max(
      MAX_USERNAME_LENGTH,
      `Username cannot exceed ${MAX_USERNAME_LENGTH} characters`,
    ),
  email: z.email("Invalid email format"),
});

type User = z.infer<typeof UserSchema>;

完全なコード:examples/core.md パターン 1


パターン 2:safeParse とエラーのフォーマット

結果は success による判別可能なユニオンなので、失敗の分岐ではエラーに、成功の分岐では型付けされたデータに絞り込まれます。

const result = UserSchema.safeParse(data);

if (!result.success) {
  const { fieldErrors } = z.flattenError(result.error);
  return { success: false, errors: fieldErrors };
}

return { success: true, user: result.data };

z.flattenError は、フラットなフォームで必要となる、1 階層のフィールドとメッセージの対応を返します。 z.treeifyError は、それより深い構造のネストを保持します。

完全なコード:examples/core.md パターン 2


パターン 3:リファインメントとフィールド間のルール

refine は 1 つの値に条件を追加します。superRefine はオブジェクト全体を参照し、エラーをどのパスに紐付けるかを選択できます。これにより、不一致のメッセージをユーザーが修正すべきフィールドに表示できます。

const PasswordFormSchema = z
  .object({
    password: z.string().min(MIN_PASSWORD_LENGTH),
    confirmPassword: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Passwords do not match",
        path: ["confirmPassword"],
      });
    }
  });

完全なコード:examples/core.md パターン 5


パターン 4:変換と入力/出力の分離

スキーマに変換を加えると、2 種類の型ができます。z.infer が返すのは出力型なので、生データを受け取る関数の引数には z.input を指定します。

const DateSchema = z.iso.datetime().transform((str) => new Date(str));

type DateInput = z.input<typeof DateSchema>; // string
type DateOutput = z.output<typeof DateSchema>; // Date

.transform() は同じスキーマ上のほかのすべてのチェックの後に実行されます。そのため、変換後の値を検査するルールは、変換の前ではなく .pipe() の後に置きます。

コード全体:examples/transforms.md のパターン 7、およびパイプについてのパターン 9


パターン 5:判別可能なユニオン

バリエーション間でリテラルフィールドを共有している場合、そのフィールドを指定すると、Zod はすべてのバリエーションではなく、該当する分岐だけをチェックできます。

const NotificationSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("email"), email: z.email(), subject: z.string() }),
  z.object({ type: z.literal("sms"), phone: z.string(), message: z.string() }),
  z.object({
    type: z.literal("push"),
    deviceId: z.string(),
    title: z.string(),
  }),
]);

通常の z.union は各メンバーをすべて試し、失敗をまとめて報告するため、「無効な入力」と表示されます。判別可能な形式ではバリエーションとその内部のフィールドを指定でき、switch で型を絞り込めます。

コード全体:examples/core.md のパターン 4


パターン 6:合成

ベースとなるスキーマから読み取り、作成、更新の各形状を導出すれば、ベースに追加したフィールドが 3 つすべてに反映されます。

const UserSchema = BaseEntitySchema.extend({
  email: z.email(),
  name: z.string(),
});

const CreateUserSchema = UserSchema.omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});
const UpdateUserSchema = CreateUserSchema.partial();
const UserSummarySchema = UserSchema.pick({ id: true, name: true });

.refine() がすでに適用されたスキーマでは .extend() を使えないため、先に拡張し、最後に絞り込みます。

コード全体:examples/core.md のパターン 3


パターン 7:文字列ソースの強制変換

URL パラメーターやフォームフィールドは、どのような値を表していても文字列として届きます。z.coerce はチェックの前に変換するため、ルールを対象の型に合わせて記述できます。

const PaginationSchema = z.object({
  page: z.coerce.number().int().positive().default(DEFAULT_PAGE),
  limit: z.coerce
    .number()
    .int()
    .positive()
    .max(MAX_LIMIT)
    .default(DEFAULT_LIMIT),
});

z.coerce.boolean() は Boolean(value) なので、空でない文字列はすべて true になります。"false" も例外ではありません。文字列が意図を表す場合は z.stringbool() を使います。

コード全体:examples/transforms.md のパターン 8


パターン 8:optional、nullable、nullish、default

値がない場合についての主張は 4 種類あります。意味に応じて選ぶことで、型を正確に保てます。

const ProfileSchema = z.object({
  name: z.string(), // required
  bio: z.string().optional(), // may be omitted — string | undefined
  avatar: z.url().nullable(), // explicitly empty — string | null
  nickname: z.string().nullish(), // either — string | null | undefined
  theme: z.string().default("light"), // absent becomes "light" — always string
});

.default() はキーが欠けている場合に値を補うため、出力型は非オプショナルのままですが、入力型はそうなりません。これも z.input と z.output が異なる理由のひとつです。


<red_flags>

注意すべき点

実行時に問題が起きます:

  • ユーザー入力に対する parse — 想定されるケースでも例外がスローされます。また、例外を受け取る catch ブロックでは、error.issues を読み取れたはずの分岐をすでに通り過ぎています。safeParse を使います。
  • 非同期のリファインメントを同期的にパース — Zod は待機せず、例外をスローします。これを含むスキーマには parseAsync または safeParseAsync を使います。
  • .refine() がすでに適用されたスキーマに対する .extend() — リファインメント済みのスキーマでは、このメソッドを使えなくなります。先に拡張し、最後に絞り込みます。
  • v4:.refine(fn, (val) => ({ message })) — 第 2 引数に関数を渡す形式は廃止されています。メッセージが値に依存する場合は superRefine を使います。
  • v4:引数が 1 つの z.record(valueSchema) — 現在はキーと値の両方のスキーマが必要です。

予想外の動作:

  • 未知のキーは拒否されず、削除されます。z.strictObject() は未知のキーを拒否し、z.looseObject() は保持します。通常のオブジェクトスキーマは何も知らせずに削除するため、名前が変更された API フィールドを見落とすおそれがあります。
  • z.coerce.boolean() は "false" を true に変換します。また、z.coerce.date() は、意図していない日付に解釈される文字列を含め、new Date() が受け付けるものをすべて受け付けます。入力が ISO 形式であるべき場合は、z.iso.datetime() を使うとそのことを明示でき、それ以外は拒否されます。
  • z.email() は空文字列を拒否するため、"" を送信するオプショナルフィールドは失敗します。明示的に許可するか、パース前に "" を undefined に正規化してください。
  • 読み取るキーがない場合、superRefine のルールは失敗するのではなく、スキップされます。オブジェクトは有効と判定されます。
  • チェックにメッセージがない場合、デフォルトの "Invalid input" メッセージがそのままユーザーに表示されます。
  • v4:superRefine から ctx.path が削除されました。ctx.addIssue({ path: [...] }) は引き続き使えます。
  • v4 での置き換え:.flatten() → z.flattenError()、.format() → z.treeifyError()、.merge() → .extend()。

これらのうち、よくある問題の修正前・修正後のコードは reference.md にあります。

</red_flags>

コメント