はじめに
このアプリでは、Supabase Functions から返ってくる JSON を受け取って画面に表示しています。
TypeScript で型は定義していたのですが、実際に返ってくる値がその型どおりかどうかまでは保証できません。
その結果、
- API のレスポンス形が少し変わる
-
nullのつもりが値が入ってこない - 数値の想定だったのに別の形で返る
といったケースで、画面側やロジック側で想定外の不具合が起きることがありました。
そこで、API レスポンスの境界で zod を使って実行時バリデーションを入れるようにしました。
この記事では、導入前に困っていたこと、zod の基本的な使い方、このプロジェクトでどう使っているかを軽くまとめます。
導入前に起きていたこと
TypeScript は便利ですが、API から返ってきた JSON の中身まで自動で検証してくれるわけではありません。
たとえば次のような状態です。
-
type User = ...と書いてあっても、JSON.parse後の値が本当にUserとは限らない - optional と nullable の揺れで実行時に落ちる
- フロント側は正しいと思って処理しているのに、レスポンスが少し違って壊れる
- どのフィールドが壊れていたのかが分かりにくい
つまり、コンパイル時の型安全性はあっても、実行時の安全性が足りない というのが課題でした。
zod の基本
zod は、スキーマ定義と実行時バリデーションをまとめて扱えるライブラリです。
やることはかなりシンプルで、
- スキーマを書く
-
parseまたはsafeParseで検証する -
z.inferで TypeScript の型も得る
という流れです。
シンプルな例
import { z } from "zod";
const userSchema = z.object({
id: z.string(),
name: z.string(),
age: z.number().int().nonnegative().optional(),
});
type User = z.infer<typeof userSchema>;
const result = userSchema.safeParse(raw);
if (!result.success) {
console.log(result.error.issues);
} else {
const user = result.data;
}
実際に導入してよかったこと
zod を入れて一番よかったのは、API の境界で「このデータは本当に信用していいか」を判定できるようになったことです。
TypeScript の型だけだと、コードを書く側は安心できますが、実際に返ってきた JSON がその型どおりかどうかまでは見てくれません。
その点、zod を通すようにしてからは、想定外のレスポンスを受け取った時点で止められるようになりました。
特に良かったのは次のあたりです。
- どのフィールドが壊れているか追いやすくなった
- 画面側では「検証済みのデータ」として扱えるようになった
- 型定義とバリデーション定義を分けずに済むようになった
- API 仕様変更の影響に気づきやすくなった
このプロジェクトでは、safeParse に失敗したときに BAD_RESPONSE 系のエラーとして扱うようにしているので、「通信には成功したけどレスポンス形式がおかしい」というケースも切り分けやすくなりました。
ハマりどころ
もちろん、zod を入れれば全部きれいに解決するわけではありません。
実際に使ってみると、いくつか気をつけるポイントもありました。
まず大きいのは、optional と nullable の扱いです。
この2つを曖昧にしたまま使い始めると、スキーマがだんだん読みにくくなります。
たとえば、
- 値が存在しないことがあるのか
- 値はあるが
nullを取りうるのか
は、似ているようで意味が違います。
API の設計とフロント側の解釈を揃えておかないと、あとで混乱しやすいです。
また、既存 API に命名揺れや微妙な仕様差があると、スキーマ側で吸収する処理が増えます。
そういうときは無理に一気に厳密化するより、まずは重要なレスポンスから順に導入した方が進めやすいと感じました。
まとめ
TypeScript はとても便利ですが、API から返ってくる値の正しさまで保証してくれるわけではありません。
そのため、外部から受け取るデータの境界で zod を使うのはかなり相性が良いです。
このプロジェクトでも、
- スキーマを定義する
-
z.inferで型を作る - API 受信時に
safeParseで検証する
という流れにしたことで、レスポンス不整合に強くなりました。
「型は書いているのに、実行時のズレで壊れることがある」という状態なら、まずは1つの API からでも zod を入れてみる価値はあると思います。