はじめに
フロントエンドでZodを使っていると、入力チェックはきれいに書けても、その定義をAPI仕様やフォーム設定へ広げるところで別管理になりがちです。結果として、「バリデーションは合っているのに、仕様書やUI側の制約がズレる」という状態が起きやすくなります。
そこで注目したいのが、Zod 4で入ったz.toJSONSchema()です。Zodで書いたスキーマをJSON Schemaへ変換できるので、1つの定義をもとに、フォーム生成、OpenAPIまわりの補助、AIの構造化出力などへつなげやすくなりました。
Zod 4は公式ドキュメント上で安定版として案内されており、JSON Schema変換とメタデータ周りがかなり実務向けになっています。この記事では、基本の使い方から、「同じスキーマを複数レイヤーで回す」ときの考え方まで整理します。
Zod 4のJSON Schema変換で何がうれしいのか
z.toJSONSchema()は、ZodスキーマをJSON Schema形式へ変換する関数です。JSON Schemaは、データ構造のルールを表すための標準的な形式で、OpenAPIやフォームライブラリ、構造化データのやり取りでよく使われます。
Zodだけで閉じているうちはparse()やsafeParse()で十分ですが、実務では「同じ制約を別の仕組みに渡したい」場面が出てきます。
たとえば次のようなケースです。
- 管理画面フォームの項目定義に流したい
- API仕様やドキュメント生成の土台にしたい
- AIのStructured Outputs向けにスキーマを整えたい
- バックエンドとフロントエンドで制約の認識を揃えたい
Zod 4では.meta()やグローバルレジストリも用意されているので、単に型を変換するだけでなく、titleやdescription、examplesも一緒に載せやすいのが実務ではかなり効きます。
基本の使い方
最初は、プロフィール編集フォームの入力定義をZodで書いて、そのままJSON Schemaへ変換する例です。このコードでは、バリデーションと説明文を1か所にまとめています。
import * as z from 'zod'
const profileSchema = z.object({
displayName: z
.string()
.min(1, '表示名は必須です')
.max(24, '24文字以内で入力してください')
.meta({
title: 'displayName',
description: '公開プロフィールに表示する名前',
examples: ['Taro Dev'],
}),
bio: z
.string()
.max(160, '160文字以内で入力してください')
.meta({
description: '自己紹介文',
examples: ['フロントエンドエンジニアです。'],
}),
newsletter: z.boolean().meta({
description: '新着情報のメール購読',
}),
})
const profileJsonSchema = z.toJSONSchema(profileSchema)
console.log(JSON.stringify(profileJsonSchema, null, 2))このコードでやっていることは2つです。まずZodで入力ルールを定義し、そのあとz.toJSONSchema()で再利用しやすい形式へ変換しています。
ここで嬉しいのは、displayNameの文字数制限や、各項目の説明文を別の場所へもう一度書かなくてよくなることです。フォーム生成や入力補助を組むとき、「元の定義はどれか」がぶれにくくなります。
便利な使いどころ
実務では、JSON Schemaへ変換した結果をそのまま公開するというより、「別レイヤーに渡すための中間表現」として使うのが扱いやすいです。
たとえば、管理画面の設定フォームを作る場面では、Zodで値を検証しつつ、JSON Schemaからラベルや説明文、必須項目の情報を拾ってUIへ渡せます。全部を自動生成しなくても、「項目定義の土台」だけ共通化できるとかなり楽です。
また、フロントエンドとバックエンドで同じスキーマを共有しているチームなら、仕様変更時のズレも減らしやすくなります。max(24)を修正したのに、ヘルプ文やAPI仕様の更新が漏れる、といった事故を減らせるのは大きいです。
応用コード
ここでは、Zodスキーマをフォーム描画用の設定へ変換するイメージを見てみます。実案件では専用ライブラリとつなぐことも多いですが、まずは「JSON Schemaを中間データにする」感覚がつかめれば十分です。
import * as z from 'zod'
const settingsSchema = z.object({
siteTitle: z.string().min(1).max(40).meta({
title: 'siteTitle',
description: '管理画面ヘッダーにも表示されます',
examples: ['Creative Coding Blog'],
}),
postsPerPage: z.int().min(10).max(100).meta({
title: 'postsPerPage',
description: '一覧画面で1ページに表示する件数',
examples: [20],
}),
showRelatedPosts: z.boolean().meta({
title: 'showRelatedPosts',
description: '記事下に関連記事を表示します',
}),
})
const jsonSchema = z.toJSONSchema(settingsSchema)
type FieldDefinition = {
name: string
label: string
description?: string
type?: string | string[]
required: boolean
}
const requiredFields = new Set(jsonSchema.required ?? [])
const fields: FieldDefinition[] = Object.entries(
jsonSchema.properties ?? {},
).map(([name, value]) => {
const property = value as {
title?: string
description?: string
type?: string | string[]
}
return {
name,
label: property.title ?? name,
description: property.description,
type: property.type,
required: requiredFields.has(name),
}
})
console.log(fields)このコードでは、Zodから作ったJSON Schemaをもとに、フォーム描画用の配列へ変換しています。フォームを完全自動生成しなくても、requiredやdescriptionを共通の定義から取れるだけで、画面追加時の手作業はかなり減ります。
特に設定画面、オンボーディング、プロフィール編集のように項目が増えやすい場所では、こういう中間層を作っておくと保守しやすいです。将来的にOpenAPIやAI向けのスキーマ連携へ広げるときも、土台を流用しやすくなります。
注意点
- すべてのZod表現がそのまま綺麗に落ちるわけではない
公式ドキュメントでも、JSON Schemaにきれいな対応先がない型や表現があると案内されています。複雑な変換ロジックは、最終的な出力を必ず確認したほうが安全です。 .meta()を先に整えると実務で効く
変換だけだと型情報中心になります。ラベルや説明文まで使いたいなら、titleやdescription、examplesをZod側へ寄せておくと再利用しやすくなります。- 生成結果をそのままUIへ直結しすぎない
実際のフォームでは、表示順、横並び、補助テキスト、条件表示など、UI固有の都合があります。JSON Schemaは共通土台として使い、最終的な見せ方は別レイヤーで持つほうが扱いやすいです。 z.fromJSONSchema()は実験的
Zod公式のJSON Schemaドキュメントでは、z.fromJSONSchema()はExperimentalとして案内されています。双方向変換を前提に組むなら、この点は先に押さえておきたいです。
まとめ
Zod 4のz.toJSONSchema()は、「バリデーション定義をその場限りで終わらせない」ための実務的な機能です。フォーム、API、ドキュメント、AI出力など、別レイヤーへ制約を渡したいときの中継点としてかなり使いやすくなっています。
まずはプロフィール編集や設定画面のような、項目数が多くて変更も入りやすい画面から試すのがおすすめです。入力制約と説明文の置き場所が1つにまとまるだけでも、想像以上に管理が楽になります。
ポイント
- Zod定義をフォームやAPI補助へ広げたいときの土台にしやすい
.meta()を併用すると説明文や例まで再利用しやすくなる- UIの見せ方まで全部自動化せず、中間表現として使うと安定する