💎

リクエストパラメーターをzodでバリデーションするときのTips

こんにちは、エンジニアの籏野です。

近年のフォルシアでの API 開発では Web フレームワークとしてExpressを利用することが多くあります。
また、開発言語には TypeScript を採用しており、型に守られた安全な開発を目指しています。
型安全な API 開発を進める上では、リクエストされたパラメーターをバリデーションし適切な型付けを担保することが必要になってきます。
このために最近はzodを使う機会が増えてきたのですが、Express と zod を組み合わせて使う際に少々ハマった点があったので、その解決方法を紹介します。
もしかしたら Express 以外の Web フレームワークでも似たような事象に遭遇するかもしれないので、Express は利用していないという方もぜひ参考にしてみてください。

また、今回の記事で扱う内容は以下のリポジトリにもまとめていますので、合わせてご覧ください。

https://github.com/taku-hatano/zod_coerce_sample

困った点

Express を利用したアプリに zod を導入した際に、クエリパラメーターに数値型の値を指定しようとした際に詰まってしまいました。
というのも、Express の標準クエリパーサーではクエリパラメーターに渡された値は全て文字列として扱われます。
そのため以下のようなバリデーションを定義してしまうと、http://localhost:3000/sample?id=1 にアクセスした際、一見バリデーションが通るように見えてしまいますが実際には id の型が合わずエラーが発生します。

const sampleSchema = z.object({
  id: z.number(),
});
app.get("/sample", (req, res) => {
  const query = sampleSchema.parse(req.query);
  res.send(query);
});

解決方法

パース前のクエリパラメーターは key と value を=で繋いだ文字列でしかないので、変換時に文字列になってしまうのは理解できますが、このままでは少々扱いづらいです。
zod を使った解決方法がいくつか考えられますので、それぞれ紹介していきます。

1. preprocess の利用

zod の preprocess メソッドを利用して、バリデーション前にパラメーターの型を変換します。

const sampleSchemaWithPreprocess = z.object({
  id: z.preprocess((val) => Number(val), z.number()),
});
app.get("/sample_with_preprocess", (req, res) => {
  const query = sampleSchemaWithPreprocess.parse(req.query);
  res.send(query);
});

数値に変換できないような値がリクエストされた場合には NaN になった後に zod によるバリデーションが行われるので、Expected number, received nanというエラーが発生します。

2. transform で型変換

先の方法とは逆に zod のバリデーションを通過した後に、型変換を行います。

const sampleSchemaWithTransform = z.object({
  id: z.string().regex(/^\d+$/).transform(Number),
});
app.get("/sample_with_transform", (req, res) => {
  const query = sampleSchemaWithTransform.parse(req.query);
  res.send(query);
});

文字列が数字の羅列であることを正規表現でチェックした後に数値に変換しています。
先の方法よりもリクエストされるべきパラメーターを厳密に定義できているように感じます。

3. coerce で型変換

最後に coerce を利用して型を強制します。
最初に紹介した preprocess と同じようにリクエストパラメータの型を変換した後に zod によるバリデーションが行われます。

const sampleSchemaWithCoerce = z.object({
  id: z.coerce.number(),
});
app.get("/sample_with_coerce", (req, res) => {
  const query = sampleSchemaWithCoerce.parse(req.query);
  res.send(query);
});

どの方法がよいのか

最近の開発では API 側で定義した zod のスキーマを元に、クライアントからリクエストすべきパラメーターの型を生成するようにしています。
この時に z.input を使っているのですが、先のそれぞれの方法で z.input の出力が少しずつ異なっています。

z.input<typeof sampleSchema>; // ->  { id: number }
z.input<typeof sampleSchemaWithPreprocess>; // ->  { id?: unknown }
z.input<typeof sampleSchemaWithTransform>; // ->  { id: string }
z.input<typeof sampleSchemaWithCoerce>; // ->  { id: number }

coerce を利用した場合には本来定義したいパラメーターと同じ型が出力されているので扱いやすく感じました。
具体的にはクライアントアプリにて以下のような実装をすることで、開発者が認識しやすい形でリクエストパラメータ―を生成することができるようになるかと思います。

  • zod スキーマに従ったオブジェクトを生成する。
  • 生成したオブジェクトをクエリパラメータ―に変換する。

最後に

今回は zod を用いた型安全な API 開発の一例を紹介しました。
zod は今回のような API 開発以外でも様々な場面で利用できてかなりありがたいライブラリになっています。

皆さんも zod 活用して、さらに快適な開発をしていきましょう!

この記事を書いた人

籏野 拓
2018 年新卒入社

秋が一瞬だった気もしますが、今年もタルトタタンを作りました。

FORCIA Tech Blog

Discussion