Arganoの池田です。前回の記事に引き続き今回の記事もZodについての記事になります。今回はZodをフォームのバリデーションなどに利用する際に必要になるエラーハンドリング関連の機能について調査した内容を書いていきます。

本記事を執筆時点でのZodのバージョンはv3.21.4です。

エラー内容の捕捉

Zodはスキーマのparseメソッドを呼び出すことでバリデーションを行うことができますが、バリデーションの結果として入力に不備があればこのメソッドはエラー内容を情報として含んだ例外を投げます。

import { z, ZodError } from "zod"

const user = z.object({
  name: z.object({
    first: z.string().min(1),
    last: z.string().min(1),
  }),
  email: z.string().email(),
})

try {
  const parsed = user.parse({ name: "", email: "" })
} catch (e) {
  if (e instanceof ZodError) {
    // ここでエラーのハンドリングを行う
  }
}

上記のコードを実行するとparse実行時に次の様なエラーオブジェクトが例外として投げられました。

{
  issues: [
    {
      "code": "too_small",
      "minimum": 1,
      "type": "string",
      "inclusive": true,
      "exact": false,
      "message": "String must contain at least 1 character(s)",
      "path": [
        "name",
        "first"
      ]
    },
    {
      "code": "too_small",
      "minimum": 1,
      "type": "string",
      "inclusive": true,
      "exact": false,
      "message": "String must contain at least 1 character(s)",
      "path": [
        "name",
        "last"
      ]
    },
    {
      "validation": "email",
      "code": "invalid_string",
      "message": "Invalid email",
      "path": [
        "email"
      ]
    }
  ]
}

ドキュメントによるとバリデーションの失敗時に投げられるエラーオブジェクトはZodErrorでありZodErrorはエラーの詳細情報をZodIssueの配列で保持していることがわかります。

ZodErrorオブジェクトの変換

エラーの発生箇所はZodIssueのプロパティであるpathにスキーマオブジェクトの当該箇所へのパスとして保存されています。エラーの情報をZodIssueの配列としてではなくて構造化されたオブジェクトで受け取りたい場合があると思います。そういった場合には自分でオブジェクトをパースすることも可能ですがZodErrorformatメソッドを利用することもできます。上記ZodErrorformatメソッドで変換したところ以下のようなオブジェクトが得られました。

{
  _errors: [],
  name: {
    _errors: [],
    first: { _errors: [ 'String must contain at least 1 character(s)' ] },
    last: { _errors: [ 'String must contain at least 1 character(s)' ] }
  },
  email: { _errors: [ 'Invalid email' ] }
}

formatメソッド以外にもflattenメソッドを使うこともできます。こちらはfieldErrors内に各バリデーション結果が展開されますが、name.firstname.lastのようなネスト下のプロパティの情報はルート直下プロパティであるnameに配列で保存されておりfirstlastというパスの情報は出力に現れなくなりました。

{
  formErrors: [],
  fieldErrors: {
    name: [
      'String must contain at least 1 character(s)',
      'String must contain at least 1 character(s)'
    ],
    email: [ 'Invalid email' ]
  }
}

エラーメッセージの変更

ここまでエラーメッセージはデフォルトのものを利用していましたが、このメッセージはZodライブラリ内部のdefaultErrorMapに定義されており、エラーの内容によって表示するメッセージが細かく定義されています。このエラーマップを変更することで表示されるエラーメッセージを変更することができます。

const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === "string") {
      return { message: `${issue.minimum}文字${issue.inclusive ? "以上で" : "より多く"}なければいけません` }
    }
  }
  return { message: ctx.defaultError }
}
z.setErrorMap(customErrorMap)

これによりparse実行後のエラー内容は以下のように変化しました。

[
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "1文字以上でなければいけません",
    "path": [
      "name",
      "first"
    ]
  },
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "1文字以上でなければいけません",
    "path": [
      "name",
      "last"
    ]
  },
  {
    "validation": "email",
    "code": "invalid_string",
    "message": "Invalid email",
    "path": [
      "email"
    ]
  }
]

上の方法ではエラーメッセージのグローバルな定義を上書きするため他のスキーマのバリデーションにも影響を与えることになりますが、スキーマ内でのみエラーメッセージを変更する方法ややパース時にカスタムしたエラーマップを渡す方法があります。

// スキーマ毎にエラーマップを上書きする
const schema: z.string({ errorMap: customErrorMap })

// パース時にエラーマップを渡す
schema.parse(input, { errorMap: customErrorMap })

以上の機能を利用することでZodでのエラーハンドリングを不自由なく実行することができます。