Zodをv3からv4へアップグレードしました
はじめに
この記事は、Commune Developers Advent Calendar 2025 シリーズ2 の18日目の記事です。
私たちのチームでは、バックエンドにおけるAPIリクエスト・レスポンスの型定義やバリデーションにZodを採用しています。
バックエンド全体でより広範囲にZodを活用していく計画があり、将来の拡張に備えてメジャーバージョンアップを実施しました。また、ZodスキーマからOpenAPI仕様書を生成するために zod-to-openapi を利用していますが、Zodのアップグレードに伴い、zod-to-openapiも併せてアップグレードしました。
更新バージョン:
- zod:
v3.23.x->v4.1.x - zod-to-openapi:
v7.3.x->v8.1.x
アップグレードの進め方
APIテストはある程度整備されているものの、バリデーションロジックの変更は本番環境での予期せぬ挙動(障害)につながるリスクがあります。そのため、一気に置き換えるのではなく、一部のAPIから段階的に移行することにしました。
1. alias による Zod v4 の並行導入
Zodをv4にアップグレードした上で、移行対象のコードではimport { z } from 'zod'でv4を利用し、それ以外の大部分のコードではimport { z } from 'zod/v3'でv3系を呼び出す形での共存も試みました。しかし、zod-to-openapiが内部で依存するZodのバージョンとの整合性が取れず、以下のコードのように拡張を行う際に問題が発生しました。
// うまくいかなかった例
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'
import { z } from 'zod/v3'
extendZodWithOpenApi(z) // 型定義の不整合が発生
そこでnpmのaliasを利用して、既存のzod(v3系)はそのままに、v4系をzod-v4という別パッケージ名として共存させる方法を選択しました。
npm install zod-v4@npm:zod@^4.1.x
"dependencies": {
"zod": "3.23.x",
"zod-v4": "npm:zod@^4.1.x"
}
Install a package under a custom alias. Allows multiple versions of a same-name package side-by-side
https://docs.npmjs.com/cli/v8/commands/npm-install
2. 一部のモジュールのみ v4 で先行リリース
影響範囲を限定するため、破壊的変更や非推奨APIを含むモジュールの一部でのみzod-v4をインポートし、先行してリリースを行いました。
// 移行対象外: 通常の 'zod' (v3系) を利用
import { z } from 'zod'
export const userSchema = z.object({
id: z.string(),
attributes: z.record(z.string()),
})
// 移行対象: aliasを使用した 'zod-v4' (v4系) を利用
import { z } from 'zod-v4'
// v4 なので record の引数は2つ必須(破壊的変更に対応した書き方)
export const productSchema = z.object({
id: z.string(),
attributes: z.record(z.string(), z.string()),
})
3. 全体をv4に移行
局所的なリリースで数日間問題がないことを確認した後、すべてのAPIをv4に移行しました。具体的には以下の作業を行いました:
-
zodパッケージをv4系にアップグレード -
zod-to-openapiをv8系にアップグレード(v4に対応したバージョン) - aliasとして導入していた
zod-v4を削除し、import { z } from 'zod'に統一
破壊的変更や非推奨APIの更新にはzod-v3-to-v4を使用しました。ただし、自動で変換されない部分もあったので、一部は手動で対応しました。
npx zod-v3-to-v4 path/to/tsconfig.json
リリース後に発覚した事象
リリース後、フロントエンド側において invalid_format - Invalid ISO date というバリデーションエラーのログ出力を確認しました。
原因
zod-to-openapiのアップグレードにより、z.date()から生成されるOpenAPI仕様書に"format": "date"が自動的に付与されるようになりました。
フロントエンドの一部で Orval を使用し、OpenAPI仕様書からZodスキーマを自動生成しています。仕様書に"format": "date"が付与されたことで、生成されるコードがz.string().date()になりました。APIのレスポンスは2024-01-01T10:00:00Zという日時形式を返却していたため、parseに失敗しバリデーションエラーログが出力されていました。
対応方法
バックエンド側のZodスキーマに対し、明示的にformat: 'date-time'を指定しました。これによりデフォルトの挙動である"format": "date"が上書きされ、Orvalで生成されるスキーマとの整合性が確保されました。
// 変更前
createdAt: z.date()
// 変更後
createdAt: z.date().openapi({ format: 'date-time' })
まとめ
- Zodのメジャーバージョンアップは、段階的なリリースとツールの活用によりスムーズに進めることができました。
- 今回初めて npm の alias を利用しましたが、段階的移行において非常に便利でした。ライブラリのバージョンアップ時の選択肢として今後も活用していきたいです。
-
zod-to-openapiのような拡張ライブラリを使用する場合、本体(Zod)との依存関係が密になりがちです。今後はバージョンアップ時の追従コストや結合度をより意識した上で、ライブラリ選定や設計を行っていきたいと思います。
Discussion