グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



Zodを使用したバリデーション実装ではまったこと【メールアドレス必須エラー 編】

フォームを作る際のスキーマをZodを使って実装しています。

最近、メールアドレスを入力するフォームを作るにあたって、痒い所に手が届かないなと感じた点がありましたので、備忘録として残しておこうかと思います。

実際に感じたむず痒い箇所

実際にスキーマ例を作るうえで、メールアドレス入力フォームの要件を以下に定義します。

  • 入力が必須
  • 許容文字数は20文字以内
  • メールアドレス形式であること

さて、愚直にこの要件のメールアドレスのバリデーションスキーマを作った場合、下記のようになるかと思います。

const schema = z
  .string()
  .min(1, { message: "メールアドレスは必須" })
  .max(20, { message: "メールアドレスは20文字以内" })
  .email({ message: "メールアドレスの形式が正しくない" });

これを下記のように検証してみると、

schema.parse("hogehogehogehogehogehoge");

最大文字数と形式のエラーは正常に動いていることが分かります。

ZodError: [
  {
    "code": "too_big",
    "maximum": 20,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "メールアドレスは20文字以内",
    "path": []
  },
  {
    "validation": "email",
    "code": "invalid_string",
    "message": "メールアドレスの形式が正しくない",
    "path": []
  }
]

さて、ここまでは良いのですが、問題は必須エラーです。

schema.parse("");

上記のように空文字の場合、必須エラーのみが出ているというのが望んでいる挙動なのですが、実際は・・・

ZodError: [
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "メールアドレスは必須",
    "path": []
  },
  {
    "validation": "email",
    "code": "invalid_string",
    "message": "メールアドレスの形式が正しくない",
    "path": []
  }
]

形式エラーも出てしまいます。。

必須入力であれば、形式エラーが出ていてもそこまで問題はないのですが、必須入力ではない場合に空文字で形式エラーが出てしまうのは問題が生じてしまいます。

下記のように先程のスキーマから必須の部分を削るだけなのですが、

const schema = z
  .string()
  .max(20, { message: "メールアドレスは20文字以内" })
  .email({ message: "メールアドレスの形式が正しくない" });

実際に空文字で検証すると

schema.parse("");
ZodError: [
  {
    "validation": "email",
    "code": "invalid_string",
    "message": "メールアドレスの形式が正しくない",
    "path": []
  }
]

当然ですよね、空文字はメールアドレスの形式とは異なりますからね。

ん?これじゃ必須じゃないのに空文字入力するとエラーが出て、フォーム送信できない?

確かに空文字はメールアドレスではないし、強固にメールアドレスのバリデーションしたいから、.email()を使ってるんですけど、Zodくん、もう少し融通利かせてくれないのかな。。

若輩エンジニアである当方のコーディングでなんとかするしかないですね。

Union型にすればどうか

const schema = z.string().email().or(z.literal(""));

このようにstring | ""のUnion型にすれば、空文字を許容することができます。

が、しかし、フォームがメールアドレス入力が必須項目であった場合なので、下記のようにして、

const schema = z
  .string()
  .min(1, { message: "メールアドレスは必須" })
  .max(20, { message: "メールアドレスは20文字以内" })
  .email({ message: "メールアドレスの形式が正しくない" })
  .or(z.literal(""));

空文字入力すると……

schema.parse("");

これは通ってしまい、必須エラーは出ないのです。
当然ですよね、空文字をUnion型で許容してるので。

このように.email()と空文字許容(必須エラー)をうまく付き合わせるには、もう少し工夫がほしいようです。

痒いところに無理やり手を届かせてみた

実施に回避した方法として

const newSchema = z
  .string()
  .refine((v) => z.string().email().or(z.literal("")).safeParse(v).success);

.refine()の内部で.email()を用いる形式をとりました。

なので、まずメールアドレス入力必須のスキーマは、下記のようにして空文字入力の場合は必須エラーだけが出るようにしました。

const newSchema = z
 .string()
 .min(1, { message: "メールアドレスは必須" })
 .max(20, { message: "メールアドレスは20文字以内" })
 .refine((v) => z.string().email().or(z.literal("")).safeParse(v).success, {
  message: "メールアドレスの形式が正しくない",
 })

▼検証結果

newSchema.parse("")
ZodError: [
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "メールアドレスは必須",
    "path": []
  }
]

許容文字数以上+メールアドレス形式でない入力の場合は、下記のようにしっかりと文字数と形式エラーが表示されています。

▼検証結果

newSchema.parse("hogehogehogehogehogehoge");
ZodError: [
  {
    "code": "too_big",
    "maximum": 20,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "メールアドレスは20文字以内",
    "path": []
  },
  {
    "code": "custom",
    "message": "メールアドレスの形式が正しくない",
    "path": []
  }
]

また、メールアドレス任意入力の場合のスキーマは下記のようにしました。

const newSchema = z
 .string()
 .max(20, { message: "メールアドレスは20文字以内" })
 .refine((v) => z.string().email().or(z.literal("")).safeParse(v).success, {
  message: "メールアドレスの形式が正しくない",
 })

空文字入力の場合は、.email()の形式エラーが出ることもなく、しっかりと空文字許容されていますね。

▼検証結果

newSchema.parse("")

まとめ

上記の回避策はあくまで一例に過ぎないので、これに固執することなく今後もこういった痒い所に手が届かないバリデーションと格闘していくので、また何かありましたら記事にしていきます。

この記事を書いた人

2G
2G
システムエンジニアへの夢をあきらめきれず、建築業界からIT業界へ転職。
アーティス入社後はフロントエンドエンジニアとして、webアプリケーションサービスの開発に従事している。趣味は、ラーメン屋巡り。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします