Zodで始めるTypeScriptバリデーション入門2026|スキーマ定義の基本からReact Hook Form連携まで

ZodのTypeScriptバリデーションライブラリを初心者向けに解説。スキーマ定義・エラーハンドリング・React Hook Form連携を3ステップで習得。実業務でのハマりポイントも正直に書いた。

ZodTypeScriptバリデーションReact Hook Formフロントエンド2026

※当サイトはアフィリエイトプログラムに参加しています。記事内のリンクから商品を購入すると、当サイトに報酬が支払われることがあります。詳しくはプライバシーポリシーをご覧ください。

「Zodってよく聞くけど、結局何をするライブラリなの?」

実は5月の連休明け、業務でZodを使う機会が急にやってきて、最初の1時間は正直「何これ」状態でした。TypeScriptの型がついてるのにさらにバリデーション?と思ったんですが、使い始めてわかった——TypeScriptの型とZodは、守る場面が全然違うんです。

結論から言うと、Zodは「実行時のバリデーション」のためのライブラリです。TypeScriptの型は「コンパイル時」にしか機能しません。APIから返ってくるJSONやユーザーの入力値は、実行時に初めて型の正しさが問われます。そこをZodが守ってくれる。

この記事では、Zodを使い始めようとしている方に向けて、3ステップで基本から実践まで解説します。コードも全部動くものを書きますので、手を動かしながら読んでいただければ。

TypeScriptコードのイメージ


ステップ1:Zodの基本スキーマを理解する

インストール

npm install zod

TypeScriptプロジェクトであれば、これだけで使えます。TypeScriptの基礎を先に押さえておくと、この後の内容がスムーズに入ってきます。JavaScriptからTypeScriptに移行するタイミングに迷っている方は、そちらも参考にどうぞ。

最初に書くスキーマ

import { z } from "zod";

// 基本的なスキーマ定義
const UserSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  age: z.number().int().min(0).max(120),
  email: z.string().email("有効なメールアドレスを入力してください"),
});

// 型の自動推論(これが便利)
type User = z.infer<typeof UserSchema>;
// 型は { name: string; age: number; email: string } になる

z.infer<typeof UserSchema> でTypeScriptの型を自動生成できるのが地味にすごくて——型定義とバリデーションを二重管理しなくていいんです。これを知るまで、僕は自分でinterfaceとZodスキーマを両方書いていました(完全に無駄な作業でした)。

parseとsafeParse

バリデーションの実行は2通りの書き方があります。

// parse: 失敗すると例外を投げる
try {
  const user = UserSchema.parse({
    name: "中村ソウマ",
    age: 33,
    email: "souma@example.com",
  });
  console.log(user); // バリデーション通過後のオブジェクト
} catch (e) {
  console.error(e); // ZodError
}

// safeParse: 失敗しても例外を投げない(推奨)
const result = UserSchema.safeParse({
  name: "",         // minが1なのでエラー
  age: 33,
  email: "not-an-email",  // emailじゃないのでエラー
});

if (!result.success) {
  console.log(result.error.issues);
  // [
  //   { message: "名前は必須です", path: ["name"] },
  //   { message: "有効なメールアドレスを入力してください", path: ["email"] }
  // ]
}

safeParseを使う方を強くおすすめします。 parseはエラー時にthrowするので、処理を止めたくない場面(APIレスポンスのバリデーション等)ではsafeParse一択です。ここがポイントなんですが、僕は最初parseばかり使っていて、後で全部safeParseに書き換えるという非効率なことをやりました。

よく使うスキーマの種類

スキーマ用途
z.string()文字列.min() .max() .email() .url()
z.number()数値.int() .min() .max() .positive()
z.boolean()真偽値そのまま使うことが多い
z.array()配列z.array(z.string())
z.object()オブジェクト複数フィールドをまとめる
z.enum()列挙z.enum(["admin", "user", "guest"])
z.optional()省略可能z.string().optional()
z.union()どちらかz.union([z.string(), z.number()])

(書いてたらコーヒー冷めた)


ステップ2:React Hook Form + Zodでフォームバリデーション

なぜReact Hook FormとZodを組み合わせるのか

React Hook Formはフォーム管理、Zodはバリデーションというのがそれぞれのコアですが、この2つを組み合わせるとスキーマ定義ひとつでフォームバリデーションが完結します。

ちょっと話が逸れますが、「React Hook Form単体でもバリデーションできるんじゃ?」と思う方へ——できます。でも、バリデーションルールをJSオブジェクトで書く方法と、Zodのスキーマ定義では後者の方がはるかに可読性が高い。長期メンテを考えるとZodと組み合わせる方が断然いいです。

セットアップ

npm install react-hook-form @hookform/resolvers
# zodは既にインストール済みの前提

実装例

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const ContactSchema = z.object({
  name: z.string().min(1, "名前を入力してください"),
  email: z.string().email("メールアドレスの形式が正しくありません"),
  message: z
    .string()
    .min(10, "メッセージは10文字以上で入力してください")
    .max(1000),
});

type ContactFormData = z.infer<typeof ContactSchema>;

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<ContactFormData>({
    resolver: zodResolver(ContactSchema), // ここでZodと繋ぐ
  });

  const onSubmit = (data: ContactFormData) => {
    // バリデーション通過後にここが呼ばれる
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="お名前" />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register("email")} type="email" placeholder="メールアドレス" />
      {errors.email && <p>{errors.email.message}</p>}

      <textarea {...register("message")} placeholder="メッセージ" />
      {errors.message && <p>{errors.message.message}</p>}

      <button type="submit">送信</button>
    </form>
  );
}

zodResolver(ContactSchema) の1行を追加するだけで、Zodのバリデーションルールがそのままフォームに適用されます。

正直なことを言うと、@hookform/resolvers のバージョンによって zodResolver のimportパスが変わることがあって、ここで15分溶かしました。import { zodResolver } from "@hookform/resolvers/zod" とパスの末尾に /zod をつけるのを忘れずに。

パスワード確認バリデーション

const SignupSchema = z
  .object({
    password: z
      .string()
      .min(8, "パスワードは8文字以上で入力してください")
      .regex(/[A-Z]/, "大文字を1文字以上含めてください")
      .regex(/[0-9]/, "数字を1文字以上含めてください"),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "パスワードが一致しません",
    path: ["confirmPassword"],
  });

.refine() でカスタムバリデーションも書けます。パスワードの一致確認とか、複数フィールドをまたぐバリデーションに使えます。


ステップ3:APIレスポンスのバリデーション

なぜAPIレスポンスにもバリデーションが必要か

TypeScriptで型アサーション(as User)を使うと、型エラーが消えます。でも実行時の保証は何もない。外部APIが返してきた値が本当に期待する形かどうかは、Zodで実際に検証するしかありません。

const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
  userId: z.number(),
});

type Post = z.infer<typeof PostSchema>;

async function fetchPost(id: number): Promise<Post | null> {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`
  );
  const json = await response.json();

  const result = PostSchema.safeParse(json);

  if (!result.success) {
    console.error("APIレスポンスの形式が期待と異なります", result.error);
    return null;
  }

  return result.data; // 型安全なPostオブジェクト
}

as Post でキャストする代わりに、safeParse で検証してから使う。これが型安全なAPIクライアントの書き方です。

認証まわりの実装でも同じ考え方が使えます。Next.js App RouterでAuth.js v5を導入した際の失敗談でも触れていますが、セッションオブジェクトの型を信頼しすぎて実行時エラーになるケースがあります。Zodでセッションデータを検証するだけでかなり防げます。

配列レスポンスの場合

const PostListSchema = z.array(PostSchema);

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const json = await response.json();

  const result = PostListSchema.safeParse(json);
  return result.success ? result.data : [];
}

z.array(PostSchema) で配列全体を一度にバリデーションできます。

ORMと組み合わせる

Zodはバリデーションだけでなく、DBから取得したデータの型安全性確保にも使えます。Drizzle ORM vs Prismaの比較記事でも触れていますが、どちらのORMもZodスキーマとの連携機能が充実してきています。PrismaにはZodスキーマを自動生成するprisma-zod-generator、DrizzleにはZodスキーマを推論するdrizzle-zodがあります。

データベースのイメージ


ここ、正直微妙だと思ったポイント

おすすめしない人: 小規模なプロジェクトや、TypeScriptを使っていないプロジェクトへのZod導入は費用対効果が低いです。バニラJavaScriptでZodだけ入れても、z.infer<typeof Schema> の型推論の恩恵を受けられないので、入れる意味が薄い。

バンドルサイズについて: Zodのバンドルサイズは約13KB(gzip後)。大きくはないですが、超軽量を求めるプロジェクトでは代替としてvalibotという選択肢もあります。ただ、ほとんどのケースでZodで十分です。

エラーメッセージのデフォルトが英語: z.string().min(1) とだけ書くと、エラーメッセージは「String must contain at least 1 character(s)」という英語になります。日本語サービスなら全部自分でカスタムメッセージを書く必要があります。少し手間ですが、慣れれば問題ありません。

「“いや、テストはどうするの?“って思った人、次で書きます」

バリデーションのロジックをZodで書くと、テストも書きやすくなります。safeParse() の結果を検証するだけなので、フロントエンドテスト Jest vs Vitest 2026年版を参考に、境界値テストを追加していくのがおすすめです。


よくある質問

Q: Zodを使う前にTypeScriptを覚える必要がありますか?

A: TypeScriptがわかっていた方が断然有利です。特に z.infer<typeof Schema> で型を自動生成する場合、ジェネリクスの基礎知識がないと「何をやっているのか」がわかりにくい。TypeScriptとJavaScriptどちらを先に学ぶべきかも参考にしてください。最低限「型定義って何をするものか」がわかった状態でZodを触るのが、学習の近道です。

Q: React Hook Formを使わなくてもZodは使えますか?

A: もちろんです。ステップ1で紹介したように、safeParse() を直接呼ぶだけでバリデーションできます。React Hook Formとの組み合わせはあくまで「フォームに特化した使い方」です。APIレスポンスのバリデーションならReact Hook Formは不要です。サーバーサイドのNode.jsや、Next.jsのAPI Routesでの入力バリデーションにも同じ書き方が使えます。

Q: バリデーションのテストはどう書けばいいですか?

A: Zodのスキーマ自体は単純なJavaScriptオブジェクトなので、JestやVitestで普通にテストできます。expect(UserSchema.safeParse(validData).success).toBe(true) という形で境界値テストを書くのが基本的なパターンです。バリデーションロジックをスキーマに集約しておくと、テストも書きやすくなります。

Q: YupというライブラリもよくZodと比較されますが、どちらがいいですか?

A: 2026年時点ではZodの方が主流になっています。理由は主に3つ。TypeScriptとの親和性が高い(型推論が優秀)、メンテナンスが活発、エコシステムが充実している(prisma-zodgen、drizzle-zod等の連携ライブラリが豊富)。既存プロジェクトがYupを使っているならそのままでも問題ありませんが、新規プロジェクトならZodを選ぶべきです。本文では触れませんでしたが、Zodは.transform() でバリデーション後にデータを変換する機能も持っていて、Yupより表現力が高い場面が多いです。


【2026年5月追記】Zod v4 RC登場

この記事を書いていた5月中旬、Zod v4のRC版がリリースされました。注目の変更点はバンドルサイズの大幅削減(約13KB → 約7KB)とパフォーマンスの大幅向上(特に大きなオブジェクトの解析速度)。破壊的変更は比較的少なく、v3からの移行コストは低そうです。ただし現時点ではRC版なので、本番プロジェクトへの導入はもう少し待ってから判断するのが無難です。情報が確定次第、記事を更新します。


あわせて読みたい

【買い切り型】Colosoでプロから学ぶ

業界トップの講師によるオンライン講座。買い切り型で何度でも復習可能。受講期限なし。

講座一覧を見る →