Zod でAPIレスポンスのバリデーションを整えたらエラー周りがキレイになった話
外部APIのレスポンスを as でキャストしていたコードに Zod を導入したところ、実行時エラーが減り、型の信頼度がかなり上がりました。その手順と感想をまとめます。
🙌 結論から
API レスポンスの型を as でキャストしていたコードに Zod を導入したら、「型は正しいはずなのに実行時に undefined になった」という事故がかなり減りました!
Zod はスキーマを書くだけで TypeScript の型も自動で推論されるので、型定義を二重に書く必要がありません。
外部 API のレスポンスや localStorage のデータなど、「型は合っているつもりだったが実際には違った」という境界に使うと、かなり効果を感じられます。
💡 以前の問題
仕事で使っていた TypeScript プロジェクトでは、外部 API からデータを取得するときにこういうコードを書いていました。
const res = await fetch('/api/user')
const data = await res.json() as User
TypeScript 的にはエラーが出ませんが、as はあくまでコンパイラへの「信じてくれ」という命令です。
実際のレスポンスに name フィールドがなかった場合でも、TypeScript はそれを検知できません(´・ω・`)
あるとき、本番環境で API のレスポンス構造が変わったことに気づかず、data.profile.email を参照する箇所で Cannot read properties of undefined エラーが出たことがありました。
TypeScript を使っているのに型の恩恵を活かせていない、という状況…
👀 Zod でバリデーションを書く
Zod はスキーマ定義から TypeScript の型を自動で生成できるライブラリです。
npm install zod
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
profile: z.object({
bio: z.string().nullable(),
}),
})
type User = z.infer<typeof UserSchema>
z.infer<typeof UserSchema> で型が自動生成されるので、型定義と実行時チェックを同時に管理できます!
実際にバリデーションをかけるときは parse か safeParse を使います。
const res = await fetch('/api/user')
const raw = await res.json()
const result = UserSchema.safeParse(raw)
if (!result.success) {
console.error(result.error.format())
throw new Error('APIレスポンスの形式が不正です')
}
const user = result.data // この時点で User 型が保証される
safeParse は例外を投げずに { success, data, error } を返してくれるので、エラーハンドリングがしやすいです(^^)
parse の方はバリデーション失敗時に例外を投げるので、try-catch で扱いたい場面に向いています。
✨ 導入してみた感想
道入して一番ありがたかったのは、APIの構造変更に即座に気づけるようになったことです。
以前は変更に気づかず UI が壊れてから発覚していたのが、リクエスト時点でバリデーションエラーとして検知できるようになりました。
.nullable() や .optional() を明示的に書く作業が、ドキュメントの代わりにもなっているので、後からコードを読む人にも伝わりやすくなりました🔥可読性が増したのはチームにとってもいいことですね^^
一点だけ注意が必要なのは、レスポンスの型が複雑な場合にスキーマの記述量がそれなりに増えることです。
特にネストが深い場合は .object() の中に .object() を入れる形になり、最初は少し読みにくさを感じるかも・_・;)
ただ、スキーマを細かく分割して z.object({ profile: ProfileSchema }) のように組み合わせると、見通しが良くなります!
🆙 Zod v4に上げてわかった変更点
しばらく運用してから Zod v4 が出ていることに気づいて、アップグレードしてみました。
一番驚いたのがパフォーマンスです。公式ベンチマークによると、文字列のパースが14倍、配列が7倍、オブジェクトが6.5倍速くなっているとのことで、私のプロジェクトでも大きめのレスポンスをパースするAPIで体感できるレベルの違いがありました(・∀・)
書き方にもいくつか変更が入っていて、私が実際に直したのはこのあたりです。
// v3 の書き方
const schema = z.object({
email: z.string().email(),
});
// v4 の書き方(フォーマット系がトップレベル関数に)
const schema = z.object({
email: z.email(),
});
.email() のようなフォーマット系のバリデーションが、文字列メソッドのチェーンからトップレベルの関数(z.email())に変わっています。
エラーの整形ヘルパーも変わっていて、以前は error.format() としていたところが z.treeifyError(error) のような独立した関数になりました。
// v3
console.error(result.error.format());
// v4
console.error(z.treeifyError(result.error));
ありがたいのは、Zod v3時代のAPIも警告付きで動き続ける設計になっていることです。
私は移行期間中、zod/v4 という新しいimportパスを使って、ファイルごとに少しずつ書き換えていく形で対応しました。
一気に全部書き換えるのは怖かったんですが、この「並行運用できる」仕組みのおかげで、テストを見ながら安全に移行できたのが助かりました!
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
👍 まとめ
Zod は 「TypeScript の型は合っているが、実行時に崩れる」という境界に使うと、けっこう便利です!s
外部 API のレスポンスはその典型例で、as でキャストするよりも safeParse で検証する方が、実際の安全性がかなり上がります。
型定義とスキーマを1か所で管理できるのも、コードの見通しを良くしてくれるので重宝できます。
まだ as を使ったキャストが多いコードがあれば、ぜひ境界部分から少しずつ Zod に置き換えてみてください(^o^)/
※ Zodのバージョンによってスキーマの書き方が異なる場合があります。最新情報は Zod公式ドキュメント をご確認ください👀