🌟

zodを使って、クエリパラメータのバリデーションを簡潔にする

2022/09/08に公開約4,000字

zodというnpmパッケージの存在をつい先日知りました。
zodを使うと非常に簡単にバリデーションチェックができてコードがスッキリしたので今回はその紹介をしたいと思います。

問題となったケース

クエリパラメータをNextサーバー側で受け取る場合、以下のように書いていました。

pages/api/sample.tsx
export default async function handler(req, res) {
  const { hogeId, numberId } = req.query
  if (!hogeId || typeof hogeId !== 'string') {
    return res.status(422).send({
      message: 'Query parameter is required',
      response: 'failureResponse',
    });
  }
  if (!fugaId || typeof fugaId !== 'string') {
    return res.status(422).send({
      message: 'Query parameter is required',
      response: 'failureResponse',
    });
  }
  // 以下適当な処理
  const response = await fetchSomething({
    hogeId: parseInt(hogeId),
    fugaId: parseInt(fugaId),
  });
}

今回のケースでは、クエリパラメータ必須のAPIなので、クエリパラメータが存在しない場合やstring以外で送られてきた場合は422エラーを返す必要があるのですが、if (!id || typeof id !== 'string')の部分が冗長で非常に見にくい。

そこで、zodを導入することになりました。

zodを使ってバリデーション部分を書き換える

zodを使うと、z.string()を使うだけで簡単に書き換えることができます。
また、zodではデフォルトでnullableがfalseになっているのも良いポイントです。

ただ、z.string()だと''のような空文字は許容しているので、そもそもapi/sampleだけで送られた場合はnullが入るので良いとして、万が一api/sample?hogeId=&fugaId=等で送られた場合にも対応できるようにminLengthを1で設定しておきます。

エラーメッセージもカスタムしたいので、messageでカスタムします。
これらを踏まえて書き換えると以下のようになりました。

pages/api/sample.tsx
export default function handler(req, res) {
  const { hogeId: _hogeId, fugaId: _fugaId } = req.query;
  const schema = z.string().min(1, { message: 'Query parameter is required' });
  try {
    const hogeId = schema.parse(_hogeId);
    const fugaId = schema.parse(_fugaId);
  } catch (error) {
    if (error instanceof ZodError) {
      return res.status(422).send({
        message: err.message,
        response: 'failureResponse',
      });
    }
  }

z.string().min(1)を満たさなかったものはcatchでエラーとして受け取ることができるので、エラー時の対応に関してもまとめて書くことができるようになりました。

受け取ったクエリパラメータをnumberに変換したい場合

クエリパラメータはstringで送られてくるので、その後にnumberに変換したいという場面もあるかと思います。
この場合も簡単で、transformを使えばいいだけです。

pages/api/sample.tsx
  const schema = z
    .string()
    .min(1, { message: 'Query parameter is required' })
    .transform((val) => parseInt(val));
   
   // 以下のhogeIdとfugaIdはnumber型になっている
   const hogeId = schema.parse(_hogeId);
   const fugaId = schema.parse(_fugaId);

簡単にバリデーションチェックと型変換ができました!

ちなみに、今回はAPIを叩く場面でstringなどは入らないようにしていることもありhogeId=aaa等になることはないのでこのままで良いのですが、もしクエリパラメータがnumberに変換できるかどうかを確認して、変換できない場合(NaNになってしまう場合)はエラーを返したいのであれば、.refine((val) => !isNaN(val))を最後につけることで解決できます。

  const schema = z
    .string()
    .min(1, { message: 'Query parameter is required' })
    .transform((val) => parseInt(val))
    .refine((val) => !isNaN(val), { message: 'Not a number' });

transformの後でrefineを実行すると、transformで変換されたあとの状態で条件を書くことになるので、refine内では再度の変換処理が不要になり条件だけを書けば良いので楽です。

また、refineを使わない場合transform内で以下のように書くこともできます。

// @refs https://github.com/colinhacks/zod#validating-during-transform

  const schema = z
  .string()
  .min(1, { message: 'query parameter is required' })
  .transform((val, ctx) => {
    const parsed = parseInt(val);
    if (isNaN(parsed)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Not a number",
      });
      return z.never;
    }
    return parsed;
  });

あとがき

初めてzodを使ってみましたが、エラー文言のカスタムやバリデーションチェックなど色々簡単に行うことができ非常に便利でした。

READMEにも載っている通り(以下参照)、メールアドレスチェックや、urlチェック、uuidチェックなど、普段は自前で正規表現を利用してバリデーションを行っているものも予め用意されていたりするので、いろんな場面で使えそうだなと思いました。

z.string().min(5, { message: "Must be 5 or more characters long" });
z.string().max(5, { message: "Must be 5 or fewer characters long" });
z.string().length(5, { message: "Must be exactly 5 characters long" });
z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().uuid({ message: "Invalid UUID" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });

いろんな利用ケースで使えて、カスタムも簡単にでき、しかも軽量という文句なしのパッケージなので、バリデーションに悩んでいる方はぜひ試してみてください!

Discussion

ログインするとコメントできます