本文へスキップ
ウェブエンジニア問題集
第17章

Zodチートシート — スキーマ・メソッド・v3対応表

約10分
この章の目次開く

最後の章は、読むためではなく引くための章です。作業中に開く前提でまとめています。

スキーマ一覧

書き方通る値
z.string()文字列(空文字も通る)
z.number()数値(NaN と Infinity は不可)
z.boolean()true / false
z.date()Date オブジェクトのみ
z.bigint()BigInt
z.array(s)配列(空配列も通る)
z.tuple([s1, s2])固定長で位置ごとに型が違う配列
z.object({...})オブジェクト(未知のキーは削除)
z.strictObject({...})オブジェクト(未知のキーはエラー)
z.looseObject({...})オブジェクト(未知のキーを保持)
z.record(k, v)キーが不定のオブジェクト
z.union([s1, s2])どれか1つに当てはまればよい
z.discriminatedUnion('type', [...])目印のキーで形が切り替わる
z.literal('a')その値のみ
z.enum(['a', 'b'])決まった選択肢
z.any() / z.unknown()何でも(検証しない)

文字列フォーマット

z.email() / z.url() / z.uuid() / z.iso.date() / z.iso.datetime() / z.iso.time() / z.ipv4() / z.ipv6() / z.jwt() / z.base64() / z.nanoid() / z.ulid() / z.cuid2()

z.email() が弾くもの・通すもの

デフォルトは実用重視の正規表現1本で、RFC 5322準拠ではありません。よく引っかかるところだけ。

入力結果理由
a@b.com / user.name+tag@example.co.jp通る
a@b / user@localhost✗TLDが必須
a@b.c✗TLDは2文字以上
user@127.0.0.1✗IPアドレスのドメインは不可
user@例え.テスト✗非ASCIIは不可
.user@example.com / us..er@example.com✗先頭のドット・連続ドット
"user@example.com "✗前後の空白(.trim() を先に挟む)
user@example-.com通るドメインラベル末尾のハイフンは見ていない
300文字のローカル部通る長さ制限がない。z.email().max(255) で自分で止める

厳しさを変えたいときは pattern を差し替えます。

z.email({ pattern: z.regexes.html5Email }) // ブラウザの input[type=email] と同じ。localhost も通る
ts

デフォルト挙動の早見表

詳しい解説は デフォルト挙動の章にあります。

入力値z.string()z.number()z.boolean()z.array()
""通る✗✗✗
" "通る✗✗✗
0✗通る✗✗
NaN✗✗✗✗
[]✗✗✗通る
null✗✗✗✗
undefined✗✗✗✗

null / undefined の許し方

書き方nullundefinedキーが無い
そのまま✗✗✗
.optional()✗通る通る
.nullable()通る✗✗
.nullish()通る通る通る
.default(v)✗v になるv になる
.catch(v)v になるv になるv になる

「空」を弾く定型

弾きたいもの書き方
空文字z.string().min(1)
空白だけの入力z.string().trim().min(1)
空配列z.array(s).min(1)

メソッド一覧

文字列

メソッド用途避けたい使い方
.min(n) / .max(n)文字数未入力対策に .min(1) だけ使う(空白が通る)
.length(n)ちょうどn文字—
.regex(re)パターン一致メールアドレスを自作の正規表現で検証する
.startsWith / .endsWith / .includes部分一致—
.trim()前後の空白削除検証の後に書く
.toLowerCase() / .toUpperCase()大文字小文字の変換—

数値

メソッド用途注意
.min(n) / .max(n)範囲—
.gt(n) / .lt(n)境界を含まない範囲—
.int()整数—
.positive()0より大きい0 が弾かれる
.nonnegative()0以上個数・在庫はこちら
.multipleOf(n)nの倍数—

オブジェクト

メソッド用途
.extend({...})キーを追加
.pick({ k: true }) / .omit({ k: true })キーの絞り込み
.partial()全キーを任意に
.required()全キーを必須に
.shape個々のキーのスキーマを取り出す
.catchall(s)未知のキーの値を検証

共通

メソッド用途
.refine(fn, 'メッセージ')独自の条件。メッセージ必須
.superRefine((v, ctx) => ...)複数のエラーを返す
.transform(fn)値を変換する(検証はしない)
.pipe(schema)別のスキーマへつなぐ
.optional() / .nullable() / .nullish()null・undefinedの許可
.default(v) / .catch(v)既定値

実行と型

書き方戻り値
schema.parse(data)検証済みの値(失敗時は ZodError を throw)
schema.safeParse(data){ success: true, data } または { success: false, error }
schema.parseAsync(data)上の非同期版(async な refine があるとき)
z.infer<typeof schema>検証後の型(= z.output)
z.input<typeof schema>検証前の型(フォームの値はこちら)

エラー処理の定型

const result = schema.safeParse(input);
if (!result.success) {
  return { errors: z.flattenError(result.error).fieldErrors };
}
// result.data は型が付いている
ts
関数返る形用途
z.flattenError(e){ fieldErrors, formErrors }1階層のフォーム
z.treeifyError(e)入れ子オブジェクトネストしたフォーム
z.prettifyError(e)複数行の文字列ログ・デバッグ

日本語化は起動時に1行です。

z.config(z.locales.ja());
ts

v3 → v4 対応表

v3v4備考
z.string().email()z.email()動くが非推奨
z.string().ip()z.ipv4() / z.ipv6()削除済み
{ required_error }{ error }黙って無視される
{ invalid_type_error }{ error }黙って無視される
{ errorMap }{ error }黙って無視される
error.format()z.treeifyError(error)メソッドも残存
error.flatten()z.flattenError(error)メソッドも残存
.strict()z.strictObject()メソッドも残存
.passthrough()z.looseObject()メソッドも残存
.nonstrict() / .deepPartial()—削除済み
太字の3つ(required_error / invalid_type_error / errorMap)だけは、エラーも警告も出ずに無視されます。移行時の最優先チェック項目です。

コピーして使うスニペット

フォーム(React Hook Form)

const schema = z.object({
  name: z.string().trim().min(1, '名前を入力してください'),
  email: z.email('メールアドレスの形式が正しくありません'),
  age: z.string().min(1, '入力してください').pipe(z.coerce.number().int().min(0)),
});
 
type FormValues = z.input<typeof schema>; // infer ではない
ts

パスワード確認

z.object({ password: z.string().min(8), confirm: z.string() }).refine(
  (d) => d.password === d.confirm,
  { message: 'パスワードが一致しません', path: ['confirm'] },
);
ts

環境変数

export const env = z
  .object({
    DATABASE_URL: z.string().min(1),
    API_BASE_URL: z.url(),
    PORT: z.coerce.number().int().positive(),
    DEBUG: z.stringbool().default(false),
  })
  .parse(process.env); // 壊れていたら起動させない
ts

APIレスポンス

const result = userSchema.safeParse(await res.json());
if (!result.success) {
  console.error(z.prettifyError(result.error));
  return null; // 画面全体を落とさず縮退させる
}
return result.data;
ts

作成用・更新用スキーマの派生

export const userSchema = z.object({ id: z.number(), name: z.string(), email: z.email() });
export const createUserSchema = userSchema.omit({ id: true });
export const updateUserSchema = createUserSchema.partial();
ts

困ったときの逆引き

症状見るところ
空文字が通ってしまう.trim().min(1) にする
null で落ちる.nullable() か .nullish()
送った項目が消えるz.object() が未知のキーを削除している
空欄が 0 になるz.coerce.number() の前に z.string().min(1)
false が true になるz.stringbool() を使う
メッセージが英語のままv3のオプションが残っていないか
エラーが画面に出ないrefine の path 指定
変換が効かない戻り値(result.data)を使う
$ZodAsyncError が出るparseAsync / safeParseAsync に変える

各項目の詳しい理由は デフォルト挙動の章と 実務パターンの章にあります。


本書はここまでです。第1章で見たとおり、TypeScriptの型はビルド時に消えます。外から届くデータを守れるのは、実行時に動く検証だけです。境界で1回止める——それだけで防げる不具合が、驚くほどたくさんあります。

参考リンク

TypeScriptクイズに挑戦するこの章で学んだ型と検証の知識を、4択クイズでアウトプットして定着させよう