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

ZodErrorの中身とエラーメッセージの日本語化

約7分
この章の目次開く

safeParse が失敗したとき、result.error に入っているのが ZodError です。既定のメッセージは英語で、開発者向けの文言なので、そのままユーザーに見せることはできません。この章でその整形方法を扱います。

issues — 失敗の一覧

ZodError の中身は issues という配列です。失敗した項目の数だけ要素が入ります。

const schema = z.object({ name: z.string().min(1), age: z.number() });
const result = schema.safeParse({ name: '', age: 'x' });
 
result.error.issues;
// [
//   { code: 'too_small', path: ['name'],
//     message: 'Too small: expected string to have >=1 characters' },
//   { code: 'invalid_type', path: ['age'],
//     message: 'Invalid input: expected number, received string' }
// ]
ts

主なプロパティです。

プロパティ内容
code失敗の種類(invalid_type / too_small / custom など)
pathどの項目か。ネストは ['user', 'name']、配列は ['tags', 1]
message人が読むメッセージ
path があるので「どの入力欄のエラーか」を機械的に振り分けられます。

整形用のヘルパー

issues の配列をそのまま画面に流すのは扱いづらいので、用途別の変換関数が用意されています。

関数返る形向いている用途
z.treeifyError(error)構造をそのまま辿れる入れ子オブジェクトネストしたフォーム
z.flattenError(error)fieldErrors / formErrors の平坦な形1階層のフォーム
z.prettifyError(error)人が読む複数行の文字列ログ・開発時の確認
z.flattenError(result.error);
// {
//   formErrors: [],
//   fieldErrors: {
//     name: ['Too small: expected string to have >=1 characters'],
//     age: ['Invalid input: expected number, received string']
//   }
// }
ts
z.treeifyError(result.error);
// { errors: [], properties: {
//     name: { errors: ['Too small: ...'] },
//     age:  { errors: ['Invalid input: ...'] } } }
ts
console.log(z.prettifyError(result.error));
// ✖ Too small: expected string to have >=1 characters
//   → at name
// ✖ Invalid input: expected number, received string
//   → at age
ts

fieldErrors は「項目名 → メッセージ配列」なので、フォームの各入力欄の下に出すのがそのまま書けます。ネストの無いフォームなら flattenError が一番扱いやすいです。

正誤を判定するイメージのイラスト

メッセージを日本語にする3つの方法

1. ロケールを適用してまとめて日本語にする

Zodには日本語ロケールが同梱されています。アプリの起動時に1回設定すれば、既定のメッセージがすべて日本語になります。

import { z } from 'zod';
 
z.config(z.locales.ja());
ts

適用の前後で、同じ検証のメッセージがこう変わります。

検証適用前適用後
z.string() に数値Invalid input: expected string, received number無効な入力: stringが期待されましたが、数値が入力されました
z.string().min(8)Too small: expected string to have >=8 characters小さすぎる値: stringは8文字以上である必要があります
z.email()Invalid email address無効なメールアドレス

2. メソッドの第2引数で指定する

個別の検証にメッセージを添えます。もっとも手軽で、実務でよく使う形です。

z.string().min(8, 'パスワードは8文字以上で入力してください');
z.array(z.string()).min(1, 'タグを1つ以上選んでください');
ts

3. スキーマ生成時に error で指定する

型そのものが違う場合(文字列を期待したのに数値が来た等)のメッセージは、スキーマの引数で指定します。

z.string({ error: '文字列で入力してください' });
ts

実務での組み立て方

APIのハンドラでは、safeParse の失敗をそのまま400として返す形が定番です。

const result = createUserSchema.safeParse(body);
 
if (!result.success) {
  return Response.json(
    { message: '入力内容を確認してください', errors: z.flattenError(result.error).fieldErrors },
    { status: 400 },
  );
}
ts

全項目分のエラーがまとめて返るので、フロントは1回のレスポンスですべての入力欄にエラーを表示できます。1件ずつ指摘して何度も送信させる、という体験を避けられます。

学習者学習者

エラーの中身をそのままレスポンスに載せて大丈夫なの?

先生先生

fieldErrors くらいなら問題ないよ。ただし error オブジェクト全体をJSONにするのは避けよう。内部のスキーマ構造が推測できる情報が混ざることがあるからね。返すのは「項目名」と「メッセージ」だけに絞るのが安全だよ。


よくあるハマりどころ

英語のメッセージがそのまま画面に出る

既定のメッセージは英語です。ロケール適用か、項目ごとのメッセージ指定が必要です。

required_error を書いても変わらない

v3の書き方です。v4では error を使います。

refine のメッセージが Invalid input になる

refineの章のとおり、refine は第2引数を省略すると Invalid input になります。

エラーが画面に出ない

path が空だと、どの入力欄にも紐づきません。オブジェクト全体に掛けた refine では path を指定してください。

ちゃんと使うためのポイント

  • ZodError の本体は issues の配列。code / path / message を持つ
  • 1階層のフォームなら z.flattenError() の fieldErrors が一番扱いやすい
  • z.config(z.locales.ja()) で既定メッセージを日本語化できるが、語彙は開発者向けのまま
  • ユーザーに見せる文言は、メソッドの第2引数か { error: '...' } で個別に書く
  • v3の required_error はv4では無視される

次の章では、フォームやURLクエリのようにすべてが文字列で届くデータを扱う z.coerceと環境変数の検証 に進みます。ここには「空欄が0になる」という有名な落とし穴があります。

参考リンク

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