Zod v4(beta)へのマイグレーションを試す
2024/4/9にZod v4 betaが公開されました。
Zod v4 betaでは処理速度やバンドルサイズの改善が含まれている一方で、ドロップしたり非推奨になったAPIも多いです。
そのため、まだbeta版であるものの試しにマイグレーションしてみたのでログを残しておきます。
あくまで私のプロジェクトで必要になったマイグレーションであり、網羅的に調査したわけではないことに注意してください。
また、beta版のため安定版と異なる挙動が含まれている可能性があります。
※ 3.22.3 -> 4.0.0-beta.20250424T163858 へのマイグレーションを試しました
リファレンス
マイグレーションで実行したこと
破壊的変更のAPIのマイグレーション
required_error / invalid_type_error の削除
Zod v4ではrequired_error
・invalid_type_error
パラメータでカスタムエラーを設定できなくなりました。
今後は必須入力違反時などにエラーをカスタムしたい場合、入力値により判定する必要があります。
z.string({
- required_error: "This field is required",
- invalid_type_error: "Not a string",
+ error: (issue) => issue.input === undefined
+ ? "This field is required"
+ : "Not a string"
});
私のプロジェクトではほとんどのケースで同じエラー文言をセットしていたためconfigを追加することにしました。
こうすることで個別のスキーマでカスタムエラーを設定する必要がなくなります。
z.config({
customError: (issue) => {
const { input, code } = issue;
if (code === "invalid_type" && (input == null || input === "")) {
return "必須";
}
return undefined;
},
});
z.string().url()を修正
これはv4からなのかわかりませんが、z.string().url()
の挙動が少し変わっていました。
今まではhttp://localhost:3000
やhttp://localhost:3000/callback
などをパースしてもエラーになりませんでしたが、v4ではエラーになりました。
いくつかの対応方針はあると思いますが、次のようにしました。
- const urlSchema = z.string().url()
+ const urlSchema = z.url().or(z.string().startWith("http://localhost"))
非推奨になったAPIのマイグレーション
z.string().uuid()などをトップレベルAPI(z.uuid())に変更
ガイドにもある通りですが、従来のz.string().uuid()
やz.string().email()
などは非推奨となりz.uuid()
、z.email()
が推奨されるようになりました。
- id: z.string().uuid(),
- email: z.string().email(),
+ id: z.uuid(),
+ id: z.email(),
nativeEnum()をnative()に変更
こちらもガイドにある通りです。
z.nativeEnum()
が非推奨となり、z.enum()
を使うように推奨されています。
const Enum = {
HOGE: "HOGE",
FUGA: "FUGA",
} as const;
- const enumSchema = z.nativeEnum(Enum)
+ const enumSchema: z.enum(Enum)
messageをerrorに変更
こちらもガイドにある通りです。
- const str = z.string().max(100, { message: "100文字以下で入力" })
+ const str = z.string().max(100, { error: "100文字以下で入力" })
ZodIssueCode
ガイドには明示されていませんが、z.ZodIssueCodeも非推奨のようです。
/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
export declare const ZodIssueCode: {
readonly invalid_type: "invalid_type";
readonly too_big: "too_big";
readonly too_small: "too_small";
readonly invalid_format: "invalid_format";
readonly not_multiple_of: "not_multiple_of";
readonly unrecognized_keys: "unrecognized_keys";
readonly invalid_union: "invalid_union";
readonly invalid_key: "invalid_key";
readonly invalid_element: "invalid_element";
readonly invalid_value: "invalid_value";
readonly custom: "custom";
};
主にtransform
やsuperRefine
などでctx.addIssue
する時に使っていましたが、「代わりにリテラルで記述して」とのことなのでそれに従いました。
ctx.addIssue({
- code: z.ZodIssueCode.custom,
+ code: "custom",
message: "送客種別が選択されていません",
});
superRefineをcheckにする
ガイドには明示されていませんが、superRefineも非推奨になりました。
Defining schemasの章で少し言及されていました。
/** @deprecated Use `.check()` instead. */
superRefine(refinement: (arg: core.output<this>, ctx: RefinementCtx<this["_zod"]["output"]>) => void | Promise<void>): this;
check
で代替するように書かれていますが、仕様次第ではrefine
でも対応できそうです。
check
はrefine
が内部的に使用しているAPIでより柔軟な設定ができます。
refine
で代替できる場合はrefine
、そうでない場合はcheck
を使うのが良さそうです。
superRefine
をcheck
で代替したサンプルです。
- import type { RefinementCtx } from "zod";
+ import { core } from "zod";
const refine = {
- data: InputData,
- ctx: RefinementCtx,
+ ctx: core.ParsePayload<InputData>
} => {
+ const data = ctx.value
if(data.length < 5) {
- ctx.addIssue({
+ ctx.issue.push({
+ input: data
code: "custom",
path: ["hoge"],
message: "カスタムメッセージ"
})
}
}
以上です!
Discussion