zodで相関チェックをする(refine)
はじめに
最近zodに入門しました。zodで相関チェックをやる方法を調べたのでその覚書です。
ケースとしては例えば開始日と終了日があって、終了日が開始日より未来かどうかをバリデーションでチェックしてエラーメッセージを出したいというような場合です。
refine
を使って以下のような感じでできました。
export const formSchema = z
.object({
startDate: z.string().refine(
(val) => {
return val.length > 0;
},
{ message: "日付を入力してください" }
),
endDate: z.string().refine(
(val) => {
return val.length > 0;
},
{ message: "日付を入力してください" }
)
})
.refine(
(args) => {
const { startDate, endDate } = args;
const startDateObject = dayjs(startDate);
const endDateObject = dayjs(endDate);
// 終了日が開始日より未来かどうか
return endDateObject.isAfter(startDateObject);
},
{
message: "終了日は開始日より未来の日付にしてください",
path: ["endDate"]
}
);
refineについて
zodは.refine
を使うことで独自のバリデーションロジックを書くことができます。
const myString = z.string().refine((val) => val.length <= 255, {
message: "String can't be more than 255 characters",
});
refine()
は2つの引数を取ります。
- 1つ目は検証ロジックです。期待する条件を書きます。parseを実行した際にこの条件がfalseの場合、エラーが返ります。
- 2つ目は複数のオプションを受け取ります。
パラメーターは以下のように定義されています。
type RefineParams = {
// override error message
message?: string;
// appended to error path
path?: (string | number)[];
// params object you can use to customize message
// in error map
params?: object;
};
パラメーター | 説明 |
---|---|
message | エラーメッセージ |
path | エラー扱いにする項目を指定できる |
params | 何かしら他に渡したい値がある場合に使えるオブジェクト |
メッセージの引数部分は以下のようにも書けます。
const longString = z.string().refine(
(val) => val.length > 10,
(val) => ({ message: `${val} is not more than 10 characters` })
);
非同期の処理も書けます。その場合は.parseAsync
メソッドを使用してパースします。
const stringSchema1 = z.string().refine(async (val) => val.length < 20);
const value1 = await stringSchema1.parseAsync("hello"); // => hello
const stringSchema2 = z.string().refine(async (val) => val.length > 20);
const value2 = await stringSchema2.parseAsync("hello"); // => throws
.transform
とチェーンすることもできます。
z.string()
.transform((val) => val.length)
.refine((val) => val > 25);
superRefineについて
.refine
よりもより複雑にカスタマイズできるのが.superRefine
です。
zodはバリデーションエラーになった場合にZodError
を返します。エラー項目の情報はZodIssue
というデータ構造でZodError
に保持されています。
superRefine
を使えば独自にIssueを作成して好きなだけ追加することができます。
const Strings = z.array(z.string()).superRefine((val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 3,
type: "array",
inclusive: true,
message: "Too many items 😡",
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `No duplicates allowed.`,
});
}
});
refine
でも以下のように複数のバリデーション項目を追加することができますが、Issueのエラーコードなどは設定できません。refine
は常にZodIssueCode.custom
エラーコードのIssueを作成します。ZodIssueCode
にIssueのコード一覧が載っています。
const arrayRefineSchema = z
.array(z.string())
.refine((val) => val.length <= 3, {
message: 'Too many items 😡',
})
.refine((val) => val.length == new Set(val).size, {
message: `No duplicates allowed.`,
});
Abort early
デフォルトではrefine
で複数のバリデーションチェックを追加した場合、前のバリデーションが失敗した後も後続のバリエーションが続行されます。しかしバリデーションエラーが発生した時点で処理を中断してしまいたい場合もあると思います。その場合は、ctx.addIssue
のfatalフラグ
をtrueにして、z.NEVER
を返すようにします。
const schema = z.number().superRefine((val, ctx) => {
if (val < 10) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "should be >= 10",
fatal: true, // ←ここ
});
return z.NEVER; // ←ここ
}
if (val !== 12) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "should be twelve",
});
}
});
さいごに
今関わっているプロダクトではjoiを使っていますが辛いと感じるころもありzodを調べていました。TypeScriptファーストでインターフェースもわかりやすく、ドキュメントもわかりやすいと思います。個人的にはこちらのほうが開発体験も向上しそうで好印象です。
Discussion
joiなんてものがあるのですね。勉強になりました。
superRefine
はエラーチェックの実行順序を制御したいとき、ないしは相関チェックなどでハンディな印象でした。すこしデモを作ってみました。
早期リターンできたほうです。
簡単ですが、以上です。