i-Vinci TechBlog
株式会社i-Vinciの技術ブログ

バリデーションライブラリzodを導入してみる

ピクミン4が出たら本気出すといった割に、体験版までで力尽きたakiyoshiです。
いいんです。他にやりたいことがあるだけなのだから。

さて本題です。
開発中のアプリに入力フォームやパラメーターがあると、フロントサイド、サーバーサイド共にバリデーションが必要になることが多いと思います。
私もTypeScriptでの開発でしばしばバリデーションを書いていますが、必要とはわかっていても、絶妙に面倒くさいです。(皆さんはどうでしょうか?)
そういう中でいい感じのライブラリを使う機会があったので、少し記します。

素直にバリデーションを組む

バリデーション処理の条件次第ですが、記述する処理自体はそこまで難易度が高くない傾向があると思います。
そのため、各項目に対して素直に1つずつ条件を書いていくことは容易です。

そうした場合に、私が気になる問題はこのあたりです。

  • 項目数が多くなってくると、記述量が多くなりがち
  • 開発者によって書きっぷりがばらける
  • 間違いを見落としやすい(とくに正規表現は)
const validator = (data: user) => {
  if (data.name.length <= 0) {
    // INVALID_NAME
  }
  if (data.name.length > 30) {
    // INVALID_NAME
  }
  if (/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(data.email)) {
    // INVALID EMAIL FORMAT
  }
  if (data.age < 0) {
    // INVALID_AGE
  }
  if (200 < data.age) {
    // INVALID_AGE
  }
    // etcetera
};

たとえば、上のように文字数チェックなどの等符号表記ゆれくらいならがんばって誤りを拾えるのですが、
emailやIPアドレスなどの正規表現が間違えていると、ぱっと見で分からない自信があります(情けない限りです。)

ライブラリzodを導入する

ということで、タイトル通りライブラリを導入するわけですが、そもそもzodとは何ぞやという話です。

What is Zod?

Zod (github) is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object.

Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.
引用元:Total TypeScript Zod Tutorial

公式からリンクされているサイトにはこのように書かれており、TypeScriptファーストを謳っているライブラリだそうです。
私は使ったことがありませんが、類似のライブラリとしてyupというもあります。

zodで実装する

zodを使って、複数項目に対するバリデーションを書いてみます。

import { z } from "zod";

type Schema = z.input<typeof schema>;
const SEX = ["MALE", "FEMALE", "OTHERS"] as const;

const schema = z.object({
  // データ型がstringであること、文字列の長さが1文字以上、30文字以下であること
  name: z.string().min(1).max(30),
  // データ型がstringであること、email形式になっていること
  email: z.string().email(),
  // データ型がnumberであること、0以上(正の数)、200以下であること
  age: z.number().nonnegative().lte(200),
  // 定義している配列の値のいずれかであること
  sex: z.enum(SEX),
});

export const validator = (data: Schema) => {
  const result = schema.safeParse(data);
  return result.success ? [] : result.error.flatten().fieldErrors;
};

どうでしょうか?
まず、schemaでバリデーションする項目と閾値を定義します。あとはそれをparse or safeParseで検証させるだけで、チェックは完了します。
schemaを見るだけでバリデーションの項目、閾値がわかるので、だいぶ分かりやすくなったのではないしょうか?
また、項目の型もチェックできるので、早めに事故に気付けるのはいいところですね。
(※ただしHTMLのinputフォームだとすべての値がstringで格納されてしまうため、型変換が必要になります。)
しれっと書きましたが、schemaから型を生成できるので、つねに利用するとは限りませんが、便利に感じます。
schema_type

意図的にバリデーションに引っ掛かるように上記のコードを実行してみるとこんな感じで、
コンソールにはfieldErrorsの結果を出力しています。
validate_result

バリデーションでできる範囲

すっきり書けるということは理解いただけたとして、気になることが2点出てきます。
1点目がどのくらいパターンが網羅されているのかです。
そこで改めて、zodのGitHubリポジトリを読むと、一般的な範囲であれば十分対応できることがわかります。
ちなみに、string型ならざっくりこのあたりの検証ができます。

  • 最大、最小、指定文字数
  • email、url、uuid、ipの形式
  • 指定文字列が含まれる、指定文字列始まり、終わり
  • 正規表現

また複雑な仕様は.refineを使えば、実現可能(結局は正規表現を書く必要は出てくる)なので、不都合は少ないかと思います。

2点目が、フォーマットチェックの仕様です。
emailのバリデーションをしたいが、定義がどうなっているのかが分からないため、意図した内容になるのかが不安ということがあり得ます。
が、zodのGitHubリポジトリには実装テストが載っており、具体的なOK、NGパターンがわかるようになっています。これで、ある程度の判断はつくのではないでしょうか?ありがたいですね。

エラーハンドリング

schemaを検証する際に利用するparse, safeParseについて軽く説明すると
parseは検証で問題があれば、ZodErrorをthrowしますが、safeParseZodErrorをthrowしません。
代わりに、実行結果のオブジェクトにsuccessというプロパティを持ち、問題有無をbooleanで返します。falseの場合だけ、errorプロパティ内にZodIssue(エラーコードやメッセージ、該当項目などの情報)を持ちます。
このあたりはプロジェクトでどちらが良いか判断して使ってください。
個人的にはむやみにthrowすると、呼び出し側が扱いづらく感じるので、throwしないsafeParseが好みです。

ちなみにデフォルトのエラーメッセージは英語(先ほど貼付したスクリーンショットの赤文字部)のため、ユーザー目線で直感的に分かりづらい内容になっています。
そのため、メッセージの変更が必要になります。
もちろん、きちんとカスタマイズする手段は提供されているので、安心してください。

該当箇所ピンポイントでメッセージを変更する場合には、以下のように引数で渡すのが分かりやすいです。

const schema = z.object({
  // データ型がstringであること、文字列の長さが1文字以上、30文字以下であること
  name: z
    .string()
    .min(1)
    .max(30, { message: "nameは30文字以下でお願いします!" })});

validate_message

また、プロジェクトの複数箇所で使い回したい場合にはZodErrorMapをカスタムすることで可能です。
さらにプロジェクト全体にそれを適用したい場合にはsetErrorMapZodErrorMapを渡すといいそうです(私は使ったことがないので、公式ドキュメントをご参照ください)

zod ERROR_HANDLING.mdCustomizing errors with ZodErrorMap 周辺が該当箇所です。

まとめ

今回は普通にHTMLで画面を作っていますが、
zod自体は別ライブラリのReact Hook Formと組み合わせることもできるので、今度はそれも弄ってみようと思ってます。

また、TypeScriptでのバリデーションに限らず、同じような課題感を持っているスゴイ人によって、便利な組み込み関数やライブラリがすでに用意されていることも多々あると思います。
なので、いきなりコーディングをするのではなく、事前に色々と調べると余計な手間が省けていいですね。
ただ書き捨てのコードではない場合には、ライブラリのメンテナンス状況は気にしておきましょう。

参考