🍔

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_errorinvalid_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:3000http://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";
};

主にtransformsuperRefineなどで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でも対応できそうです。
checkrefineが内部的に使用しているAPIでより柔軟な設定ができます。

refineで代替できる場合はrefine、そうでない場合はcheckを使うのが良さそうです。
superRefinecheckで代替したサンプルです。

- 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