🦁

Zodでtypescriptのバリデーションしてみた

2024/08/26に公開

初めましてfutonkimochiiです。
JINSはまだ三か月で、ITスキルは知識:応用情報、経験:ろくになし、というレベルです。
これから開発などで実践的な経験を積んでいきたいところです!

さて本記事では、typescriptのバリデーションライブラリの一つであるZodを取り上げます。Zodの使用方法は既に広く紹介されていますが、「経験:ろくになし」の私目線でお話しすることで、バリデーションライブラリって簡単に入れられるんだな~と思っていただければ幸いです。

なお実際のアプリケーションコードは公開できないため一部変更/マスクしており、そのままでは動かないことをご了承ください。

ライブラリ導入前

まずはバリデーションライブラリを導入する前の状態について説明します。

対象のアプリケーションは、REST APIで受け付けたデータ(user)を連携しているDBに保存する、というごく一般的なものとします。

Userインターフェースの定義とバリデーションがこちらです。

// 国名はこの3つのどれかを受け付ける
export type Country = 'JP' | 'US' | 'CN' ;

export const isCountry = (obj: any): obj is Country => {
  return ['JP', 'US', 'CN'].includes(obj);
};

export interface User {
    id: string;
    first_name: string;
    last_name: string;
    phone_number: number;
    email?: string;
    date_of_birth?: string;
    gender?: string;
    country?: Country;
}

// あるオブジェクトがUserインターフェースの定義を守っているか確認する
export const isUser = (obj: any): obj is User => {
  return (
    typeof obj.id === 'string' &&
    typeof obj.first_name === 'string' &&
    typeof obj.last_name === 'string' &&
    typeof obj.phone_number === 'number' &&
    (typeof obj.email === 'undefined' ||
        typeof obj.email === 'string') &&
    (typeof obj.date_of_birth === 'undefined' ||
        typeof obj.date_of_birth === 'string') &&
    (typeof obj.gender === 'undefined' ||
        typeof obj.gender === 'string') &&
    (typeof obj.country === 'undefined' ||
        isCountry(obj.country));
  );
};

インターフェースを定義して、プロパティごとに条件を当てはめています。

さらにAPIの実装では、受け取ったボディの余剰プロパティを削るため、以下のようにボディから各パラメータを取り出しています。

// リクエストボディの型チェック
if (!isUser(body)) {
    throw new BadRequestError();
}

// bodyのパラメータを取り出す
const {
    id,
    first_name,
    last_name,
    phone_number,
    email,
    date_of_birth,
    gender,
    country
} = body;

// DBにレコードを作成する
const result = await /*データベース接続オブジェクト*/.create({
    data: {
        id,
        first_name,
        last_name,
        phone_number,
        email,
        date_of_birth,
        gender,
        country
    },

もちろんこれでも動作には全く問題はありませんが、テストするときや後で変更を加えたいときに少し苦労するかもしれないと思い、バリデーションライブラリを導入することにしました。

ライブラリを使うことで、

  • コードを短くできるので、可読性・保守性が向上する
  • バリデーションの抜け漏れを防げる

というメリットがあります。一方で

  • 学習コスト
  • 脆弱性
  • バージョン管理

には注意する必要があります。

Zodの概要

typescriptのバリデーションライブラリはいろいろあるようです。どれがいいか、Chatgptに聞きました。

細かいアレができる/コレができないはありそうですが、今回は学習コストが低く・軽量シンプルなものを気軽に導入したかったのでZodを採用しました。
またgithubのスター数で見ても広く使われていそうなので、比較的信頼度が高いと判断できます。

Zodレポジトリでは以下のように特徴を説明しています。

ZodはTypeScriptファーストのスキーマ宣言・検証ライブラリである。 スキーマという用語は、単純な文字列から複雑なネストされたオブジェクトまで、あらゆるデータ型を広く指すために使っている。 Zodは可能な限り開発者に優しいように設計されている。 目標は、重複した型宣言をなくすことだ。 Zodを使えば、バリデータを一度宣言するだけで、Zodが自動的に静的なTypeScriptの型を推論してくれる。 単純な型を複雑なデータ構造にまとめるのも簡単だ。

スキーマという、オブジェクトの「あるべき姿」を定義することでバリデーションを行ってくれるのが特徴のようですね。(とはいっても私はほかのバリデーションライブラリも使ったことがないので、他と比べることはできませんが..)

ちなみに初学者の感覚では、Zodはかなりドキュメントが手厚くて読みやすいと思いました。詳しいけど難しくはない感じがGoodです。
また自分で手を動かして遊べるエクササイズ集(おそらく非公式なので注意)もあったので、体で覚えたい方はこちらもやってみるとよいかもしれません。

Zodを使ってみる

まずはプロジェクトにZodをインストールします。

    // yarnならこっち
    yarn add zod
    // npmならこっち
    npm install zod

基本は以下の流れのようです。

スキーマを定義する

では先ほどのUserインターフェースをスキーマとして再定義してみます。

import { z } from 'zod';

--

// 国名はこの3つのみ受け付ける
export const CountrySchema = z
    .string()
    .refine((val) =>
    ['JP', 'US', 'CN'].includes(val)
  );

// スキーマの定義
export const UserSchema = z.object({
    id: z.string().max(10);
    first_name: z.string().max(20);
    last_name: z.string().max(20);
    phone_number: z.number.length(11);
    email: z.optional(string());
    date_of_birth: z.optional(string());
    gender: z.optional(string());
    country: CountrySchema;
});

スキーマの定義によって、特にバリデーションの条件式がなくなったことで可読性が向上したのではないでしょうか。文字列の最大長なども簡単に定義できます。
何より重要なのは、z.という字面がなんかかっこいいということです。

パースする

ボディをオブジェクトに格納するコードはこんな感じです。

// ボディをuserオブジェクトに格納する(バリデーションも兼ねている)
const user = UserSchema.parse(body);

// DBにレコードを作成する
const result = await /*データベース接続オブジェクト*/.create({ data: user });

こちらもすっきりしたことがわかると思います。
特に余剰プロパティがやってきても自動でそぎ落としてくれるので、各プロパティを取り出す必要がないところがいいですね。

ちなみに余剰プロパティというのは、リクエストボディに入っている余計なプロパティのことです

{
  "id": "12345678",
  "first_name": "futon",
  "last_name": "kimochii",
  "phone_number": "12345678900",
  "email": "futon@example.com",
  "date_of_birth": "1970-01-01",
  "gender": "male",
  "country": "JP",
  "favorite_food": "sushi" // これが余剰プロパティ
}

動作確認

バリデーションエラーはこんな感じになります。(必須プロパティが無いケース)

デフォルトでは余剰プロパティがあってもエラーを出ませんでしたが、スキーマの定義に.strictをつければエラーを投げる設定にできるようです。(参考)

デフォルトでは、Zodのオブジェクトスキーマはパース時に認識されないキーを削除します。未知のキーを許可しないようにするには、.strict() を使用します。入力に未知のキーが含まれている場合、Zodはエラーをスローします。

結果のまとめ

  • バリデーションライブラリを使うと、型定義とバリデーションがすっきりすることがわかりました。
  • 今回のような簡単なケースでとりあえず導入するにはZodはとても向いています。学習コストはほぼ0でした。

今後の展望

今回はほかのバリデーションライブラリを全く使えていないので、ぜひ次の機会には試して比較したいです。

JINSテックブログ

Discussion