💎

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を利用して、既存のzodv3系)はそのままに、v4系をzod-v4という別パッケージ名として共存させる方法を選択しました。

npm install zod-v4@npm:zod@^4.1.x
package.json
"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に移行しました。具体的には以下の作業を行いました:

  1. zodパッケージをv4系にアップグレード
  2. zod-to-openapiv8系にアップグレード(v4に対応したバージョン)
  3. 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