Zodのエラーハンドリングについて覚書
はじめに
Zodのエラーまわりについて調べたことの覚書です。
parseとエラー
Zodはparseメソッドでスキーマに定義したバリデーションを実行します。不正があった場合はZodError
をスローします。
safeparseを使うとスローエラーをせずにオブジェクト形式で結果を受け取ることができます。
import { z } from "zod";
// creating a schema for strings
const mySchema = z.string();
// parsing
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError
// "safe" parsing (doesn't throw error if validation fails)
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
ZodError
- 全てのバリデーションエラーは
ZodError
のインスタンスを返します -
ZodError
はError
を継承しています -
ZodError
は,ZodIssue
の配列であるissues
プロパティを持っています。 各イシューには検証中に発生した内容が記されています。
class ZodError extends Error {
issues: ZodIssue[];
}
独自のエラーインスタンスを作成できます
import * as z from "zod";
const myError = new z.ZodError([]);
ZodIssue
基本的なプロパティ
- code
- イシューコード
- path
- エラーの項目 e.g, ['addresses', 0, 'line1']
- message
- エラーメッセージ
ただし、エラーによっては追加のプロパティも存在する場合があります。
エラーコードの一覧とそれぞれ持っているプロパティはドキュメントの表に詳しく載っています。
実際の型定義
ZodIssue
を見てみるとZodIssueOptionalMessage
に対してfatal
とmessage
を拡張した型になっています。
ZodIssueOptionalMessage
を見てみると以下のようになっています。デフォルトのイシューが定義されています。
さらに個々のイシューの定義を見てみると以下のようのになっています。 イシューはZodIssueBase
を継承し、code
とそのイシュー固有のプロパティが拡張されています。
ZodIssueBase
にはpath
とmessage
が定義されています。
Example
以下のようなスキーマを定義します。
const person = z.object({
names: z.array(z.string()).nonempty(), // at least 1 name
address: z.object({
line1: z.string(),
zipCode: z.number().min(10000), // American 5-digit code
}),
});
不正なデータをパースさせます。
try {
person.parse({
names: ["Dave", 12], // 12 is not a string
address: {
line1: "123 Maple Ave",
zipCode: 123, // zip code isn't 5 digits
extra: "other stuff", // unrecognized key
},
});
} catch (err) {
if (err instanceof z.ZodError) {
console.log(err.issues);
}
}
ZodError
がスローされ、以下のようにissues
にエラーの内容が格納されています。
[
{
code: "invalid_type",
expected: "string",
received: "number",
path: ["names", 1],
message: "Invalid input: expected string, received number",
},
{
code: "unrecognized_keys",
keys: ["extra"],
path: ["address"],
message: "Unrecognized key(s) in object: 'extra'",
},
{
code: "too_small",
minimum: 10000,
type: "number",
inclusive: true,
path: ["address", "zipCode"],
message: "Value should be greater than or equal to 10000",
},
];
エラーマップ
zodではエラーマップによりデフォルトでエラーメッセージが定義されています。
ZodErrorMap
で独自の定義を作り、それをsetErrorMap
でセットすることでエラーメッセージをカスタマイズできます。
import { z } from "zod";
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "bad type!" };
}
}
if (issue.code === z.ZodIssueCode.custom) {
return { message: `less-than-${(issue.params || {}).minimum}` };
}
return { message: ctx.defaultError };
};
z.setErrorMap(customErrorMap);
ZodErrorMap
はissue
とctx
の2つの引数を取り、{ message: string }
を返します。
- issue: バリデーションで不正だった項目の情報
- ctx
- defaultError: デフォルトのエラーマップによって生成されるエラーメッセージ
- data:
.parse
に渡されたデータ
上記のサンプルの場合は、ZodIssueCode.invalid_type
とZodIssueCode.custom
のエラーメッセージを変更し、それ以外はctx.defaultError
の内容を返します。
エラーマップの優先順位
エラーマップをカスタマイズするには3つの方法があり、優先順位は以下のようになっています。
Global error map
< Schema-bound error map
< Contextual error map
Global error map
z.setErrorMap
でグローバルエラーマップをカスタマイズできます(優先順位が一番低い)
const myErrorMap: z.ZodErrorMap = /* ... */;
z.setErrorMap(errorMap);
サンプル
/**
* グローバルマップのカスタマイズ
*/
const errorMap: z.ZodErrorMap = (error, ctx) => {
switch (error.code) {
case z.ZodIssueCode.invalid_type:
if (error.expected === 'string') {
return { message: `Global error map` };
}
break;
}
return { message: ctx.defaultError };
};
z.setErrorMap(errorMap);
// スキーマ定義
export const FormData = z.object({
name: z.string(),
contactInfo: z.object({
email: z.string().email(),
phone: z.string().optional(),
}),
});
const sampleData = {
name: null,
contactInfo: {
email: 'not an email',
phone: '867-5309',
},
};
// バリデーション実行
const parsed = FormData.safeParse(sampleData);
const { error } = parsed as any;
console.log('issues:', (error as any).issues);
実行結果
console.log
issues: [
{
code: 'invalid_type',
expected: 'string',
received: 'null',
path: [ 'name' ],
message: 'Global error map' // 上書きされている
},
{
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: [ 'contactInfo', 'email' ]
}
]
Schema-bound error map
個別のスキーマでカスタマイズする。Global error map
より優先されます。
z.string({ errorMap: myErrorMap });
// this creates an error map under the hood
z.string({
invalid_type_error: "Invalid name",
required_error: "Name is required",
});
サンプル
/**
* グローバルマップのカスタマイズ
*/
const errorMap: z.ZodErrorMap = (error, ctx) => {
switch (error.code) {
case z.ZodIssueCode.invalid_type:
if (error.expected === 'string') {
return { message: `Global error map` };
}
break;
}
return { message: ctx.defaultError };
};
z.setErrorMap(errorMap);
// スキーマ定義
const FormData = z.object({
name: z.string({
/**
* 定義時にエラーマップをカスタマイズ
*/
errorMap: (error, ctx) => {
switch (error.code) {
case z.ZodIssueCode.invalid_type:
if (error.expected === 'string') {
return { message: `Schema-bound error map` };
}
break;
}
return { message: ctx.defaultError };
},
}),
contactInfo: z.object({
email: z.string().email(),
phone: z.string().optional(),
}),
});
const sampleData = {
name: null,
contactInfo: {
email: 'not an email',
phone: '867-5309',
},
};
// バリデーション実行
const parsed = FormData.safeParse(sampleData);
const { error } = parsed as any;
console.log('issues:', (error as any).issues);
実行結果
console.log
issues: [
{
code: 'invalid_type',
expected: 'string',
received: 'null',
path: [ 'name' ],
message: 'Schema-bound error map' // 上書きされている
},
{
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: [ 'contactInfo', 'email' ]
}
]
Contextual error map
parseメソッドのパラメーターとしてエラーマップを渡せます。 このエラーマップが提供されている場合は、最も優先度が高くなります。
z.string().parse("adsf", { errorMap: myErrorMap });
サンプル
/**
* グローバルマップのカスタマイズ
*/
const errorMap: z.ZodErrorMap = (error, ctx) => {
switch (error.code) {
case z.ZodIssueCode.invalid_type:
if (error.expected === 'string') {
return { message: `Global error map` };
}
break;
}
return { message: ctx.defaultError };
};
z.setErrorMap(errorMap);
// スキーマ定義
const FormData = z.object({
name: z.string({
/**
* 定義時にエラーマップをカスタマイズ
*/
errorMap: (error, ctx) => {
switch (error.code) {
case z.ZodIssueCode.invalid_type:
if (error.expected === 'string') {
return { message: `Schema-bound error map` };
}
break;
}
return { message: ctx.defaultError };
},
}),
contactInfo: z.object({
email: z.string().email(),
phone: z.string().optional(),
}),
});
const sampleData = {
name: null,
contactInfo: {
email: 'not an email',
phone: '867-5309',
},
};
// バリデーション実行
const parsed = FormData.safeParse(sampleData, {
/**
* 実行時にエラーマップをカスタマイズ
*/
errorMap: (error, ctx) => {
switch (error.code) {
case z.ZodIssueCode.invalid_type:
if (error.expected === 'string') {
return { message: `Contextual error map` };
}
break;
}
return { message: ctx.defaultError };
},
});
const { error } = parsed as any;
console.log('issues:', (error as any).issues);
実行結果
console.log
issues: [
{
code: 'invalid_type',
expected: 'string',
received: 'null',
path: [ 'name' ],
message: 'Contextual error map' // 上書きされている
},
{
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: [ 'contactInfo', 'email' ]
}
]
エラーのフォーマット機能
ZodErrorの構造をフォームで使いやすくするため以下のメソッドが用意されています。
.format()
.flatten()
項目の内容が不正の場合
例えば以下のようなスキーマを定義します。
const FormData = z.object({
name: z.string(),
contactInfo: z.object({
email: z.string().email(),
phone: z.string().optional(),
}),
});
以下のように各項目に間違ったデータを入れて実行してみます。
const sampleData = {
name: null,
contactInfo: {
email: 'not an email',
phone: '867-5309',
},
};
const parsed = FormData.safeParse(sampleData);
const { error } = parsed as any;
console.log('issues:', (error as any).issues);
console.log('format: ', error.format());
console.log('flatten: ', error.flatten());
実行結果は以下のようになります。
console.log
issues: [
{
code: 'invalid_type',
expected: 'string',
received: 'null',
path: [ 'name' ],
message: 'Expected string, received null'
},
{
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: [ 'contactInfo', 'email' ]
}
]
console.log
format: {
_errors: [],
name: { _errors: [ 'Expected string, received null' ] },
contactInfo: { _errors: [], email: { _errors: [Array] } }
}
console.log
flatten: {
formErrors: [],
fieldErrors: {
name: [ 'Expected string, received null' ],
contactInfo: [ 'Invalid email' ]
}
}
.format()
のほうは_errors
が空になっていて、各項目名がキーになったオブジェクトのほうにエラー内容が入っています。
.flatten()
のほうはformErrors
が空になっていて、fieldErrors
の中に各項目名がキーになったオブジェクトが入っています。
ルートレベルで不正なデータの場合
次にデータを null にして実行してみます。
const sampleData = null;
const parsed = FormData.safeParse(sampleData);
const { error } = parsed as any;
console.log('issues:', (error as any).issues);
console.log('format: ', error.format());
console.log('flatten: ', error.flatten());
実行結果は以下のようになります。
console.log
issues: [
{
code: 'invalid_type',
expected: 'object',
received: 'null',
path: [],
message: 'Expected object, received null'
}
]
console.log
format: { _errors: [ 'Expected object, received null' ] }
console.log
flatten: { formErrors: [ 'Expected object, received null' ], fieldErrors: {} }
.format()
はルートレベルのエラーの場合_errors
にエラーの内容が記されます。
.flatten()
はルートレベルのエラーの場合formErrors
にエラーの内容が記されてます。この場合fieldErrors
は空になっています。
フォーマットのカスタマイズ
.flatten()
と.format()
はどちらも、マッピング関数 (issue: ZodIssue) => U to flatten()
を渡すことで各ZodIssueが最終出力でどのように変換されるかをカスタマイズできます。
result.error.flatten((issue: ZodIssue) => ({
message: issue.message,
errorCode: issue.code,
}));
/*
{
formErrors: [],
fieldErrors: {
name: [
{message: "Expected string, received null", errorCode: "invalid_type"}
]
contactInfo: [
{message: "Invalid email", errorCode: "invalid_string"}
]
},
}
*/
型シグネチャの抽出
z.inferFormattedError
、z.inferFlattenedErrors
を使用して、.format()
と.flatten()
の戻り値の型シグネチャを取得できます。
type FormattedErrors = z.inferFormattedError<typeof FormData>;
/*
{
name?: {_errors?: string[]},
contactInfo?: {
_errors?: string[],
email?: {
_errors?: string[],
},
phone?: {
_errors?: string[],
},
},
}
*/
type FlattenedErrors = z.inferFlattenedErrors<typeof FormData>;
/*
{
formErrors: string[],
fieldErrors: {
email?: string[],
password?: string[],
confirm?: string[]
}
}
*/
第2引数でカスタマイズしたフォーマットの型定義を指定できます。
type FormDataErrors = z.inferFlattenedErrors<
typeof FormData,
{ message: string; errorCode: string }
>;
/*
{
formErrors: { message: string, errorCode: string }[],
fieldErrors: {
email?: { message: string, errorCode: string }[],
password?: { message: string, errorCode: string }[],
confirm?: { message: string, errorCode: string }[]
}
}
*/
備考
Discussion