Zod

TypeScriptValidationSchemaType InferenceRuntime Validation

GitHub概要

colinhacks/zod

TypeScript-first schema validation with static type inference

ホームページ:https://zod.dev
スター40,431
ウォッチ75
フォーク1,649
作成日:2020年3月7日
言語:TypeScript
ライセンス:MIT License

トピックス

runtime-validationschema-validationstatic-typestype-inferencetypescript

スター履歴

colinhacks/zod Star History
データ取得日時: 2025/10/22 09:49

Zod

概要

ZodはTypeScript-firstのスキーマ宣言とバリデーションライブラリです。型推論により、一度スキーマを定義すれば、Zodが自動的にTypeScriptの型を推論します。これにより、実行時の検証と静的型付けの両方を単一のスキーマ定義から得ることができ、型の安全性を保証しながら外部データの検証を行えます。

特徴

  • TypeScript-first設計: 完全な型推論サポート
  • ゼロ依存: 外部ライブラリに依存しない
  • 小さなバンドルサイズ: コアバンドルは2kb (gzip圧縮後)
  • イミュータブルAPI: メソッドチェーンによる宣言的なスキーマ定義
  • 包括的な検証: プリミティブ型から複雑なオブジェクトまで対応
  • エラーハンドリング: 詳細なエラー情報とカスタマイズ可能なメッセージ
  • JSON Schema変換: スキーマをJSON Schemaに変換可能
  • トランスフォーメーション: バリデーション後のデータ変換機能

インストール

npm install zod
# または
yarn add zod
# または
pnpm add zod
# または
bun add zod

使用例

基本的な使用方法

import { z } from 'zod';

// スキーマの定義
const UserSchema = z.object({
  name: z.string(),
  age: z.number().min(0).max(150),
  email: z.string().email(),
  isActive: z.boolean().default(true),
});

// TypeScriptの型を推論
type User = z.infer<typeof UserSchema>;
// { name: string; age: number; email: string; isActive: boolean; }

// データの検証
try {
  const userData = UserSchema.parse({
    name: "山田太郎",
    age: 30,
    email: "[email protected]"
  });
  console.log("検証成功:", userData);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error("検証エラー:", error.errors);
  }
}

// 安全な検証(例外を投げない)
const result = UserSchema.safeParse(inputData);
if (result.success) {
  console.log("有効なデータ:", result.data);
} else {
  console.log("検証エラー:", result.error);
}

高度なスキーマ定義

// 列挙型
const StatusSchema = z.enum(["pending", "processing", "completed", "failed"]);

// ユニオン型
const IdSchema = z.union([z.string(), z.number()]);

// 条件付きスキーマ
const ProductSchema = z.object({
  type: z.enum(["physical", "digital"]),
  name: z.string(),
  price: z.number().positive(),
  weight: z.number().optional(), // physicalの場合のみ必要
  downloadUrl: z.string().url().optional(), // digitalの場合のみ必要
}).refine((data) => {
  if (data.type === "physical" && !data.weight) {
    return false;
  }
  if (data.type === "digital" && !data.downloadUrl) {
    return false;
  }
  return true;
}, {
  message: "物理製品には重量が、デジタル製品にはダウンロードURLが必要です"
});

// 配列とタプル
const TagsSchema = z.array(z.string()).min(1).max(5);
const CoordinatesSchema = z.tuple([z.number(), z.number()]); // [緯度, 経度]

// Record型とMap型
const ScoresSchema = z.record(z.string(), z.number());
const ConfigSchema = z.map(z.string(), z.any());

// 再帰的スキーマ
type Category = {
  name: string;
  subcategories: Category[];
};

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

トランスフォーメーション

// 文字列を数値に変換
const StringToNumberSchema = z.string().transform((val) => parseInt(val, 10));

// 日付の処理
const DateSchema = z.string().datetime().transform((str) => new Date(str));

// カスタム変換とバリデーション
const PasswordSchema = z.string()
  .min(8, "パスワードは8文字以上必要です")
  .refine((password) => /[A-Z]/.test(password), {
    message: "大文字を1文字以上含む必要があります",
  })
  .refine((password) => /[0-9]/.test(password), {
    message: "数字を1文字以上含む必要があります",
  })
  .refine((password) => /[!@#$%^&*]/.test(password), {
    message: "特殊文字を1文字以上含む必要があります",
  });

// オブジェクトの正規化
const UserInputSchema = z.object({
  name: z.string().trim(),
  email: z.string().email().toLowerCase(),
  age: z.union([z.string(), z.number()]).transform((val) => {
    if (typeof val === "string") return parseInt(val, 10);
    return val;
  }),
});

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

const LoginSchema = z.object({
  username: z.string({
    required_error: "ユーザー名は必須です",
    invalid_type_error: "ユーザー名は文字列である必要があります",
  }).min(3, { message: "ユーザー名は3文字以上必要です" }),
  
  password: z.string({
    required_error: "パスワードは必須です",
  }).min(8, { message: "パスワードは8文字以上必要です" }),
});

// グローバルエラーマップのカスタマイズ
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    if (issue.expected === "string") {
      return { message: "文字列を入力してください" };
    }
  }
  if (issue.code === z.ZodIssueCode.custom) {
    return { message: `カスタムエラー: ${issue.message}` };
  }
  return { message: ctx.defaultError };
};

z.setErrorMap(customErrorMap);

API統合の例

// Express.jsでの使用例
import express from 'express';

const app = express();
app.use(express.json());

const CreateUserSchema = z.object({
  body: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    age: z.number().int().positive(),
  }),
});

// ミドルウェアとして使用
const validateRequest = (schema: z.ZodSchema) => {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });
    
    if (!result.success) {
      return res.status(400).json({
        error: "検証エラー",
        details: result.error.flatten(),
      });
    }
    
    req.body = result.data.body;
    next();
  };
};

app.post('/users', validateRequest(CreateUserSchema), (req, res) => {
  // req.bodyは型安全
  const user = req.body; // { name: string; email: string; age: number }
  // ユーザー作成処理
  res.json({ success: true, user });
});

フォーム検証との統合

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

const FormSchema = z.object({
  username: z.string().min(3, "ユーザー名は3文字以上"),
  email: z.string().email("有効なメールアドレスを入力してください"),
  age: z.number().min(18, "18歳以上である必要があります"),
  acceptTerms: z.boolean().refine((val) => val === true, {
    message: "利用規約に同意する必要があります",
  }),
});

type FormData = z.infer<typeof FormSchema>;

function RegistrationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(FormSchema),
  });

  const onSubmit = (data: FormData) => {
    console.log("フォームデータ:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} placeholder="ユーザー名" />
      {errors.username && <p>{errors.username.message}</p>}
      
      <input {...register("email")} placeholder="メールアドレス" />
      {errors.email && <p>{errors.email.message}</p>}
      
      <input {...register("age", { valueAsNumber: true })} type="number" placeholder="年齢" />
      {errors.age && <p>{errors.age.message}</p>}
      
      <label>
        <input {...register("acceptTerms")} type="checkbox" />
        利用規約に同意する
      </label>
      {errors.acceptTerms && <p>{errors.acceptTerms.message}</p>}
      
      <button type="submit">登録</button>
    </form>
  );
}

環境変数の検証

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  PORT: z.string().transform((val) => parseInt(val, 10)),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});

// 環境変数を検証
const env = EnvSchema.parse(process.env);

// 型安全な環境変数のエクスポート
export const config = {
  nodeEnv: env.NODE_ENV,
  port: env.PORT,
  databaseUrl: env.DATABASE_URL,
  jwtSecret: env.JWT_SECRET,
  redisUrl: env.REDIS_URL,
  logLevel: env.LOG_LEVEL,
} as const;

比較・代替手段

類似ライブラリとの比較

  • Yup: より成熟しているが、TypeScript統合が弱い
  • Joi: Node.js向けに最適化されているが、ブラウザでは重い
  • io-ts: 関数型プログラミング指向で、学習曲線が急
  • Superstruct: よりシンプルだが、機能が限定的
  • Valibot: より軽量だが、エコシステムが小さい

Zodを選ぶべき場合

  • TypeScriptプロジェクトで型推論を重視
  • 実行時検証と静的型付けの統一が必要
  • モダンなAPIとDXを求める
  • フロントエンドとバックエンドで同じ検証ロジックを共有したい
  • JSON Schemaとの相互運用が必要

学習リソース

まとめ

ZodはTypeScriptエコシステムにおける最も人気のあるバリデーションライブラリの一つです。型推論の優れたサポート、包括的な機能セット、優れた開発者体験により、TypeScriptプロジェクトでの実行時検証の標準的な選択肢となっています。特に、APIの入力検証、フォームバリデーション、環境変数の検証など、外部データの型安全な処理が必要なあらゆる場面で威力を発揮します。