TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

バリデーションライブラリZodを紹介

当社でエンジニアとしてフロントエンド開発を担当し始めてから1年ほどが経過しました。今回は最近の開発業務で使用し始めたライブラリのZodについて紹介しようと思います。

久守 大地(ヒサモリ ダイチ)
新卒3年目のフロントエンドエンジニアです。
バイク、特にハーレーが好きでいつか乗れることを夢見て日々頑張っています。


Zodとは

ユーザが入力する値が適切な値かどうかチェックするために利用することができるバリデーションライブラリです。またバリデーションだけでなく、TypeScriptの型定義に利用することもできます。

公式ドキュメント

基本的な使い方

次にZodの使い方を簡単に説明します。色々な使い方が用意されていますが、すべて紹介していると日が暮れそうなので一部抜粋しての紹介です。

まず、以下のようにschemaと呼ばれる値のバリデーションを定義します。

import { z } from "zod";

const mySchema = z.string();

上記の例ではzodからzというオブジェクトをimportし、そのzに入っているstring()メソッドを使用しています。このschemaでバリデーションする場合、値がstring型であるかチェックされます。 他にもnumberやbooleanなど基本的な型のバリデーションを実装するためのメソッドが標準で用意されています。

さらに各データ型のschema毎にさまざまな便利メソッドも用意されています。以下はstringのschemaに用意されたメソッドです。

z.string();              // 単純な文字列
z.string().min(5);       // 5文字以上の文字列
z.string().max(5);       // 5文字以下の文字列
z.string().length(5);    // 固定幅の文字列
z.string().email();      // メールアドレス文字列
z.string().url();        // URL文字列
z.string().uuid();       // UUID文字列
z.string().regex(regex); // 正規表現にマッチする文字列
z.string().nonempty();   // 空文字列以外の文字列

また、オブジェクトのschemaもネストしたり値をオプショナルにしたりと自由度が高く定義することもできます。

import { z } from "zod";

const mySchema = z.object({
  key1: z.string(),
  key2: z.number().optional(),
  key3: z.object({ nestKey: z.boolean() })
);

次にこの定義したschemaをどのように使うかを簡単に説明します。使い方には2種類あります。

parse

まず1つ目はparseです。
parseの場合、バリデーションが成功するとresultにはバリデーションを通過した値が入ります。一方失敗の場合には例外がthrowされます。

const validateValue = (value: string) => {
  try {
    const result = mySchema.parse(value);
    return result;
  } catch(error) {
    console.error(error);
  }
}

例外のerrorにはZodError型のオブジェクトが入っており、その中のissuesにエラー内容が入っています。非常に多くの情報を返してくれるので親切です🙆‍♂️

// ZodError.issues
[
   {
     "code": "invalid_type", // エラータイプ
     "expected": "string",   // 期待した型
     "received": "number",   // 受け取った値の型
     "path": [],      // エラーが発生したプロパティへのパス
     "message": "Expected string, received number" // エラー内容(schema定義の段階でカスタマイズ可能)
   }
 ]

safeParse

次に2つ目のsafeParseです。
こちらの場合、バリデーション結果には成功、失敗を問わずオブジェクトが入ります。オブジェクトのsuccessには成功したかどうかのフラグがbooleanで入っており、dataには成功の場合バリデーションを通過した値、失敗の場合にはZodError型のオブジェクトが入ります。

エラーの扱いがparseと比べて単純なので個人的にはこちらの方法が好きです🙆‍♂️

const validateValue = (value: string) => {
  const result = schema.safeParse(value);
  // 成功時{ success: true, data: "fish" }
  // 失敗時{ success: false, data: ZodError }
  return result;
}

基本的な使い方の説明はこのぐらいにしておこうと思います。

Zodを使わない場合との比較

次に実際にZodを使わなかった場合と使った場合で比較してみたいと思います。
まず、Zodを使わずに値のバリデーションをしている例を以下に示します。

条件

  • URL形式の文字列
  • 必須
  • 300文字以内
const REGEXP_URL = /^(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/;
const validateUrl = (value: string) => {
  if (value === '') {
    return '必須です';
  }
  if (!REGEXP_URL.test(value)) {
    return 'URL形式で入力してください';
  }
  if (value.length > 300) {
    return '300文字以内で入力してください';
  }
  return '';
}

条件に一致しているかどうかをifでひとつずつ確認し、一致していない場合に対応するメッセージを返す実装になっています。これをZodで書くと以下のようになります。

//schema.ts
import { z } from "zod";

export const urlSchema = z
  .string()
  .nonempty({ message: '必須です' })
  .url({ message: 'URL形式で入力してください' })
  .max(300, { message: '300文字以内で入力してください' });
import { urlSchema } from './schema';

const validateUrl = (value: string) => {
  const result = urlSchema.safeParse(value);
  if (!result.success) {
    return result.error.errors[0].message;
  }
  return '';
}

上記例ではschemaは別ファイルのschema.tsに記載し、validate関数本体はschemaをimportして使うだけのシンプルな実装です。

コード量的にはさほど差はありませんが、バリデーションに関する内容がschemaで1箇所でまとめて定義できているので全体としてはより理解しやすいコードになったと思います🙆‍♂️ また、個人的な感想にはなりますが、schemaを定義する工程がパズル感覚で楽しいです👍

Zodの便利メソッド

基本的な使い方や使用前後の比較を行いましたが、最後に、私が個人的に便利だと感じたZodのメソッドを2つ紹介します。

1つ目はrefine()です。こちらは、Zodで提供されているバリデーションだけでは実現が難しい場合に、自分でバリデーションを定義するために使います。以下、簡単な例を示します。 

const mySchema = z.string().refine(
  (val) => val.length > 10,
  (val) => ({ message: `${val}は10文字以下です` })
);

上記の例は、11文字以上の文字列のバリデーションを定義したschemaです。
refineメソッドの第一引数には、引数で対象の文字列を受け取り、boolean値を返す関数を渡します。返り値がtrueの場合、バリデーションは成功、falseの場合は失敗が結果に入ります。関数の中の処理は自由に記述できるので独自の複雑な条件を作ることも可能です。
refineメソッドの第二引数には、エラーの内容、もしくは引数で対象の文字列を受け取りエラーの内容を返す関数を渡すことができます。関数で渡してもいいのでvalueを使って自由にメッセージをカスタマイズすることも可能です。
たいていのバリデーションの実装はZodが標準で用意しているメソッドで事足りますが、二値比較など標準では対応できない場面で活躍します!

2つ目はtransform()です。こちらはバリデーション後に値を変換したい場合に使用します。

const mySchema = z.string().transform((val) => val.length);
mySchema.parse("sampleText"); // => 10

上記の例ではschemaに渡した文字列をバリデーションしたのちに文字列の長さに変換をかけているので、結果には変換後の文字列の長さが入ります。例のような実装は実際の開発ではおそらく使わないと思います(笑)
しかし、たとえばユーザーが入力した値をバリデーションしたあとで特定の形式に変換をかけてから保存したいといった場合に非常に便利だと思います。
また、transform()は変換だけでなく、バリデーションも同時に行うこともできます🙆‍♂️

const mySchema = z.string().transform((value, ctx) => {
  const parsedValue = parseInt(value);
  if (isNaN(parsedValue)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "数値を入力してください",
    });
    return z.NEVER;
  }
  return parsedValue;
});

例では、文字列を受け取って変換していますが、そのままreturnせずに変換後の値がNaNの場合はバリデーションエラーとするような実装になっています。transform()の処理中にバリデーションに失敗したことをZodに伝えるには、引数から受け取ったctx に対し、addIssue()メソッドで失敗した内容を登録する必要があります。ctx.addIssue()メソッドを処理中で複数回呼ばれた場合、ZodErrorオブジェクト中のerrorsにエラー内容が追加されていきます。addIssue()が一度も実行されなかった場合にバリデーションは成功とみなされます。また、関数の戻り値に関しては正常系の場合変換後の値を返し、異常系の場合 z.NEVER を返します。

まとめ

今回は最近開発業務でも使い始めたZodについて紹介しました。
紹介した内容以外にもschemaから型を生成できたりと他にも数多くの機能があり、使いこなせているというにはまだまだといった状況です。
Zodは比較的新しいライブラリです。この記事がZodを導入しようか迷っている方々の一助となれば幸いです。

シナジーマーケティング株式会社では一緒に働く仲間を募集しています。