Zod

バリデーションライブラリTypeScriptスキーマ型安全性ランタイム静的型推論

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 schema validation with static type inference」として開発されたバリデーションライブラリです。TypeScriptの型システムと完全に統合され、ランタイムでのデータ検証と静的型安全性を同時に提供します。スキーマ定義から自動的にTypeScript型を推論し、コンパイル時とランタイムの両方で型安全性を保証。2025年現在、AI開発やモダンWebアプリケーションにおいて最も注目されているTypeScriptバリデーションライブラリです。

詳細

Zod 4は2025年現在の最新安定版で、TypeScript-firstのアプローチにより「型とバリデーションの単一ソース」を実現します。従来のTypeScriptは静的型チェックのみでランタイム型安全性を提供しませんが、Zodはこの問題を解決。スキーマからTypeScript型を自動生成し、ランタイムでの入力データ検証を同一のスキーマで実行します。31k+のGitHubスターを獲得し、tRPC、Next.js、AI開発エコシステムで広く採用されています。

主な特徴

  • TypeScript型推論: スキーマ定義から自動的にTypeScript型を生成
  • ランタイム型安全性: 実行時のデータ型検証でTypeScript型システムを補完
  • ゼロ依存関係: 外部依存なしで軽量かつ高速
  • 関数型アプローチ: イミュータブルなスキーマオブジェクトによる安全な操作
  • 豊富な型サポート: プリミティブ、オブジェクト、配列、Union、関数まで幅広い型対応
  • カスタムバリデーション: 高度なバリデーションロジックの柔軟な定義

メリット・デメリット

メリット

  • TypeScript開発者にとって最も直感的なAPI設計
  • コンパイル時とランタイムの型安全性を同時に保証
  • AI開発やtRPCエコシステムでの標準的選択肢
  • tree-shakingによる最小バンドルサイズ
  • 豊富なコミュニティエコシステム(NextJS、Remix等で採用)
  • エラーメッセージの詳細性とカスタマイズ性

デメリット

  • TypeScript専用(純粋なJavaScriptでは使用不可)
  • 学習コストがやや高い(特に複雑なスキーマ定義)
  • 大規模なスキーマでのパフォーマンスオーバーヘッド
  • 他言語での相互運用性が限定的
  • 型推論の複雑性により開発環境への負荷
  • 純粋なJavaScriptプロジェクトでは恩恵が少ない

参考ページ

書き方の例

インストールと基本セットアップ

# Zodのインストール
npm install zod
yarn add zod
pnpm add zod
bun add zod
deno add npm:zod

# TypeScript 5.5以降が必要
npm install -D typescript

基本的なスキーマ定義とバリデーション

import { z } from "zod/v4";

// 基本的なスキーマ定義
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email(),
});

// 型推論の自動生成
type User = z.infer<typeof UserSchema>;
// type User = { name: string; age: number; email: string; }

// データのバリデーション
const userData = { name: "田中太郎", age: 30, email: "[email protected]" };

// parse - 失敗時は例外をthrow
try {
  const validUser = UserSchema.parse(userData);
  console.log(validUser); // { name: "田中太郎", age: 30, email: "[email protected]" }
} catch (error) {
  console.error("バリデーションエラー:", error);
}

// safeParse - 失敗時も例外を投げない
const result = UserSchema.safeParse(userData);
if (result.success) {
  console.log("バリデーション成功:", result.data);
} else {
  console.log("バリデーション失敗:", result.error);
}

// プリミティブ型の例
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();

stringSchema.parse("Hello"); // "Hello"
numberSchema.parse(42); // 42
booleanSchema.parse(true); // true

高度なバリデーションルールとカスタムバリデーター

// 文字列の詳細バリデーション
const PasswordSchema = z.string()
  .min(8, "パスワードは8文字以上である必要があります")
  .max(50, "パスワードは50文字以下である必要があります")
  .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, "パスワードは大文字、小文字、数字を含む必要があります");

// 数値の範囲指定
const AgeSchema = z.number()
  .int("年齢は整数である必要があります")
  .min(0, "年齢は0以上である必要があります")
  .max(150, "年齢は150以下である必要があります");

// 配列とオブジェクトの組み合わせ
const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  price: z.number().positive(),
  categories: z.array(z.string()),
  metadata: z.record(z.string(), z.any()).optional(),
});

// Union型(複数の型の中から一つ)
const StatusSchema = z.union([
  z.literal("pending"),
  z.literal("completed"),
  z.literal("failed"),
]);

// Enum型の定義
const RoleSchema = z.enum(["admin", "user", "guest"]);

// カスタムバリデーション
const CustomEmailSchema = z.string().refine(
  (email) => {
    // カスタムバリデーションロジック
    return email.includes("@") && email.endsWith(".com");
  },
  {
    message: "有効な.comドメインのメールアドレスを入力してください",
  }
);

// 複数条件の詳細バリデーション
const UserRegistrationSchema = z.object({
  username: z.string()
    .min(3, "ユーザー名は3文字以上である必要があります")
    .max(20, "ユーザー名は20文字以下である必要があります")
    .regex(/^[a-zA-Z0-9_]+$/, "ユーザー名は英数字とアンダースコアのみ使用可能です"),
  
  email: z.string().email("有効なメールアドレスを入力してください"),
  
  password: PasswordSchema,
  
  confirmPassword: z.string(),
  
  agreeToTerms: z.boolean().refine(val => val === true, {
    message: "利用規約に同意する必要があります",
  }),
}).refine(data => data.password === data.confirmPassword, {
  message: "パスワードが一致しません",
  path: ["confirmPassword"],
});

フレームワーク統合(React、NestJS、FastAPI等)

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

const UserFormSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("有効なメールアドレスを入力してください"),
  age: z.number().min(18, "18歳以上である必要があります"),
});

function UserForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(UserFormSchema),
  });

  const onSubmit = (data: z.infer<typeof UserFormSchema>) => {
    console.log("送信データ:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="名前" />
      {errors.name && <span>{errors.name.message}</span>}
      
      <input {...register("email")} placeholder="メールアドレス" type="email" />
      {errors.email && <span>{errors.email.message}</span>}
      
      <input {...register("age", { valueAsNumber: true })} placeholder="年齢" type="number" />
      {errors.age && <span>{errors.age.message}</span>}
      
      <button type="submit">送信</button>
    </form>
  );
}

// tRPCとの統合例
import { z } from "zod";
import { publicProcedure, router } from "./trpc";

const CreateUserInput = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().min(0),
});

export const appRouter = router({
  createUser: publicProcedure
    .input(CreateUserInput)
    .mutation(async ({ input }) => {
      // inputは自動的に型安全
      const user = await createUser(input);
      return user;
    }),
});

// Express.jsでのミドルウェア例
import express from "express";

const validateRequest = (schema: z.ZodType) => {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    try {
      schema.parse(req.body);
      next();
    } catch (error) {
      res.status(400).json({ error: "バリデーションエラー", details: error });
    }
  };
};

app.post("/users", validateRequest(UserRegistrationSchema), (req, res) => {
  // req.bodyは型安全にアクセス可能
  res.json({ message: "ユーザー作成成功" });
});

エラーハンドリングとカスタムエラーメッセージ

// 詳細なエラーハンドリング
const schema = z.object({
  name: z.string().min(2, "名前は2文字以上である必要があります"),
  age: z.number().min(0, "年齢は0以上である必要があります"),
});

const result = schema.safeParse({ name: "A", age: -1 });

if (!result.success) {
  console.log("エラー詳細:", result.error.errors);
  /*
  [
    {
      code: "too_small",
      minimum: 2,
      type: "string",
      inclusive: true,
      exact: false,
      message: "名前は2文字以上である必要があります",
      path: ["name"]
    },
    {
      code: "too_small",
      minimum: 0,
      type: "number",
      inclusive: true,
      exact: false,
      message: "年齢は0以上である必要があります",
      path: ["age"]
    }
  ]
  */
}

// カスタムエラーメッセージのグローバル設定
z.setErrorMap((issue, ctx) => {
  switch (issue.code) {
    case z.ZodIssueCode.invalid_type:
      if (issue.expected === "string") {
        return { message: "文字列を入力してください" };
      }
      break;
    case z.ZodIssueCode.too_small:
      if (issue.type === "string") {
        return { message: `${issue.minimum}文字以上入力してください` };
      }
      break;
    default:
      return { message: ctx.defaultError };
  }
  return { message: ctx.defaultError };
});

// フォーム用エラーハンドリングヘルパー
function getFieldErrors(error: z.ZodError): Record<string, string> {
  const fieldErrors: Record<string, string> = {};
  
  error.errors.forEach((err) => {
    if (err.path.length > 0) {
      const fieldName = err.path.join(".");
      fieldErrors[fieldName] = err.message;
    }
  });
  
  return fieldErrors;
}

// 使用例
const validationResult = schema.safeParse(userData);
if (!validationResult.success) {
  const fieldErrors = getFieldErrors(validationResult.error);
  console.log(fieldErrors); // { "name": "名前は2文字以上である必要があります", "age": "年齢は0以上である必要があります" }
}

型安全性とTypeScript統合

// 高度な型推論の例
const NestedSchema = z.object({
  user: z.object({
    profile: z.object({
      name: z.string(),
      settings: z.record(z.string(), z.boolean()),
    }),
    posts: z.array(z.object({
      id: z.number(),
      title: z.string(),
      tags: z.array(z.string()),
    })),
  }),
  metadata: z.object({
    created: z.date(),
    updated: z.date().optional(),
  }),
});

type NestedType = z.infer<typeof NestedSchema>;
/*
type NestedType = {
  user: {
    profile: {
      name: string;
      settings: Record<string, boolean>;
    };
    posts: Array<{
      id: number;
      title: string;
      tags: string[];
    }>;
  };
  metadata: {
    created: Date;
    updated?: Date | undefined;
  };
}
*/

// 関数の型安全なバリデーション
const createUserFunction = z.function()
  .args(z.string(), z.number(), z.boolean().optional())
  .returns(z.object({ id: z.string(), success: z.boolean() }));

type CreateUserFunction = z.infer<typeof createUserFunction>;
// type CreateUserFunction = (arg_0: string, arg_1: number, arg_2?: boolean) => { id: string; success: boolean; }

// 実装の型安全性
const createUser = createUserFunction.implement((name, age, isActive = true) => {
  // TypeScriptが引数の型を自動推論
  return {
    id: `user_${Date.now()}`,
    success: true,
  };
});

// 条件付きスキーマ(discriminated unions)
const AnimalSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("dog"),
    breed: z.string(),
    bark: z.boolean(),
  }),
  z.object({
    type: z.literal("cat"),
    indoor: z.boolean(),
    meow: z.string(),
  }),
]);

type Animal = z.infer<typeof AnimalSchema>;
// TypeScriptは自動的にunion型を推論

// 変換(transform)と前処理(preprocess)
const StringToNumberSchema = z.string()
  .transform(val => parseInt(val, 10))
  .refine(val => !isNaN(val), "有効な数値文字列を入力してください");

const PreprocessedSchema = z.preprocess(
  (input) => {
    // 前処理: 文字列をトリムして小文字に変換
    if (typeof input === "string") {
      return input.trim().toLowerCase();
    }
    return input;
  },
  z.string().min(1)
);

// 遅延評価(Lazy evaluation)- 再帰的なスキーマ
interface Category {
  id: string;
  name: string;
  parent?: Category;
  children: Category[];
}

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

APIスキーマとドキュメント生成

// OpenAPI/JSON Schema生成例
import { zodToJsonSchema } from "zod-to-json-schema";

const UserAPISchema = z.object({
  name: z.string().describe("ユーザーの名前"),
  email: z.string().email().describe("ユーザーのメールアドレス"),
  age: z.number().min(0).max(150).describe("ユーザーの年齢"),
  role: z.enum(["admin", "user", "guest"]).describe("ユーザーの役割"),
});

// JSON Schemaに変換
const jsonSchema = zodToJsonSchema(UserAPISchema, "UserAPI");
console.log(JSON.stringify(jsonSchema, null, 2));

// APIドキュメント生成用のメタデータ
const APIEndpoints = {
  createUser: {
    method: "POST",
    path: "/users",
    requestSchema: UserAPISchema,
    responseSchema: z.object({
      id: z.string().uuid(),
      ...UserAPISchema.shape,
      createdAt: z.date(),
    }),
  },
  getUser: {
    method: "GET",
    path: "/users/:id",
    paramsSchema: z.object({
      id: z.string().uuid(),
    }),
    responseSchema: UserAPISchema.extend({
      id: z.string().uuid(),
      createdAt: z.date(),
      updatedAt: z.date(),
    }),
  },
};

// 型安全なAPIクライアント生成
type APIClient = {
  [K in keyof typeof APIEndpoints]: (
    ...args: any[]
  ) => Promise<z.infer<typeof APIEndpoints[K]["responseSchema"]>>;
};