レビュー待ち·難易度: 中級·更新: 2026-04-18

Zod でネストオブジェクト・配列・transform を組み合わせてバリデーションする

Zod でネストされたオブジェクト・配列のバリデーションと、transform / refine / superRefine を使ったカスタム変換・クロスフィールド検証の例。

nextjsvalidationzod

対応バージョン

nextjs 15react 19zod 3

前提環境

Zod の z.string() / z.object() など基本スキーマを理解していること

概要

Zod でネストされたオブジェクト・配列の型安全なバリデーションと、transform による値変換・refine / superRefine によるクロスフィールド検証を組み合わせる。z.object() の基本から一歩進んだスキーマ設計パターンを示す。

インストール

npm install zod

実装

ネストオブジェクト

import { z } from "zod";

// ネストしたオブジェクトスキーマ
const addressSchema = z.object({
  zipCode: z.string().regex(/^\d{7}$/, "郵便番号は7桁の数字"),
  prefecture: z.string().min(1, "都道府県は必須"),
  city: z.string().min(1, "市区町村は必須"),
});

const userSchema = z.object({
  name: z.string().min(1, "名前は必須"),
  age: z.number().int().min(0).max(150),
  address: addressSchema, // ネスト
});

type User = z.infer<typeof userSchema>;
// { name: string; age: number; address: { zipCode: string; prefecture: string; city: string } }

配列バリデーション

// 配列の要素にスキーマを適用
const orderSchema = z.object({
  items: z
    .array(
      z.object({
        productId: z.string().uuid(),
        quantity: z.number().int().min(1, "数量は1以上"),
        price: z.number().nonnegative(),
      })
    )
    .min(1, "1件以上の商品が必要"),
  note: z.string().max(200).optional(),
});

// バリデーション
const result = orderSchema.safeParse({
  items: [{ productId: "550e8400-e29b-41d4-a716-446655440000", quantity: 2, price: 1000 }],
});

transform(値変換)

// 入力は string、出力は number に変換
const priceSchema = z.object({
  // フォームから来た文字列を数値に変換
  price: z
    .string()
    .regex(/^\d+$/, "数値のみ")
    .transform((val) => Number(val)),

  // 前後の空白をトリム
  name: z.string().transform((val) => val.trim()),

  // YYYY-MM-DD 文字列を Date に変換
  publishedAt: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD 形式")
    .transform((val) => new Date(val)),
});

type PriceInput = z.input<typeof priceSchema>;
// { price: string; name: string; publishedAt: string }

type PriceOutput = z.output<typeof priceSchema>;
// { price: number; name: string; publishedAt: Date }

refine(単フィールドカスタム検証)

const passwordSchema = z.object({
  password: z
    .string()
    .min(8, "8文字以上")
    .refine(
      (val) => /[A-Z]/.test(val),
      "大文字を1文字以上含めてください"
    )
    .refine(
      (val) => /[0-9]/.test(val),
      "数字を1文字以上含めてください"
    ),
});

superRefine(クロスフィールド検証)

// パスワード確認のようなフィールド間の検証
const signUpSchema = z
  .object({
    email: z.string().email(),
    password: z.string().min(8),
    confirmPassword: z.string(),
    startDate: z.string(),
    endDate: z.string(),
  })
  .superRefine((data, ctx) => {
    // パスワード一致チェック
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "パスワードが一致しません",
        path: ["confirmPassword"],
      });
    }

    // 日付の前後チェック
    if (new Date(data.startDate) >= new Date(data.endDate)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "終了日は開始日より後にしてください",
        path: ["endDate"],
      });
    }
  });

Route Handler でのバリデーション例

// app/api/orders/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";

const createOrderSchema = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().min(1),
  })).min(1),
  couponCode: z.string().optional(),
});

export async function POST(request: Request) {
  const body = await request.json();
  const result = createOrderSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json(
      { error: result.error.flatten() },
      { status: 400 }
    );
  }

  // result.data は型安全
  const { items, couponCode } = result.data;
  return NextResponse.json({ items, couponCode, status: "created" }, { status: 201 });
}

ポイント

  • ネストオブジェクトは z.object() をそのまま入れ子にするだけ。スキーマを変数に分けて z.infer<> で型を取り出せる
  • transform は入力と出力の型を変える。フォーム(文字列)→ DB(数値/Date)の変換に使う。z.input<>z.output<> で入出力の型を区別できる
  • refine は単一フィールドのカスタムチェック、superRefine は複数フィールドを参照したクロスフィールドチェックに使う。どちらも safeParse.error.flatten() でフィールドごとのエラーを取り出せる
  • zod-discriminated-union との使い分け: リクエスト種別によってスキーマ全体が変わる場合は discriminated union、同一スキーマ内の複雑なバリデーションはこのパターン
  • error.flatten(){ fieldErrors: {...}, formErrors: [...] } 形式でエラーを構造化してくれる。API レスポンスとクライアントへの表示に使いやすい

注意点

zod-form-schema-reuse はスキーマ再利用パターン。zod-api-response-validation は API レスポンス検証。zod-discriminated-union は判別ユニオン。これはネストオブジェクト・配列・transform / refine / superRefine の組み合わせに特化。

関連サンプル