JSON → Zod 変換
JSON から Zod スキーマを生成し、それぞれに z.infer の型を付けます。配列要素をマージし、任意キーと nullable を明示。サンプルから推測はしません。
import * as z from "zod"
export const Customer = z.object({
name: z.string(),
email: z.string(),
phone: z.null(),
})
export type Customer = z.infer<typeof Customer>
export const LineItem = z.object({
sku: z.string(),
quantity: z.number(),
price: z.number(),
note: z.string().nullable(),
backordered: z.boolean().optional(),
})
export type LineItem = z.infer<typeof LineItem>
export const Root = z.object({
id: z.string(),
customer: Customer,
lineItems: z.array(LineItem),
tags: z.array(z.unknown()),
})
export type Root = z.infer<typeof Root>
Zod 4 の構文で、Zod 3 でも有効です。z.object は列挙していないキーを取り除くため、サンプルになかったフィールドは、解析したデータから何も言わずに消えます。
以下の各位置では、サンプルに null しかなかったため、スキーマはそこで null 以外を受け付けません。
位置: 1
Customer.phone
以下の各位置では、どの数値も整数でした。z.number() は 7.5 も受け付け、.int() を付けると整数が必須になります。値が整数でなければならないと分かっている箇所にだけ、ご自身で追加してください。
位置: 1
LineItem.quantity
以下の各位置では何も推論できなかったため、スキーマはそこにあるものを検査しません。z.unknown() はどんな値も、z.record(z.string(), z.unknown()) はどんなオブジェクトも受け付けます。
位置: 1
Root.tags[]
データが届くたびに実行される検査
TypeScript の型はコードのコンパイル時に検査され、コードが実行される頃には消えています。Zod スキーマは実行時まで残る部分です。プログラムの中で動き、届くペイロード — API のレスポンス、Webhook の本文、キューから取り出したメッセージ — を、コードがそれに頼る前に一つずつ検査します。その JSON のサンプルを貼り付けると、このページがモジュールにそのまま貼り付けられるスキーマを書き出します。キーを持つオブジェクトはどれも名前付きの "z.object" になり、配列の要素は一つにマージされ、各スキーマの後にはそれが生む TypeScript の型が続きます。
これは、「JSON → TypeScript 変換」のページが同じ JSON に対して出す答えを、型ではなくバリデータとして綴ったものです。二つのページは一つの推論を読むので、どのキーが任意か、どの値が union になるか、null を欠けたキーとどこで区別するか、ネストした各オブジェクトを何と呼ぶかは一度だけ決まり、二通りに書き出されるだけです。こうしたマージの規則は「JSON → TypeScript 変換」のページにあるガイドの主題であり、ここでは繰り返しません。このガイドが扱うのは、実行されたスキーマが本物のデータに対して何をするか、そして何をあなたの判断に残すかです。
何が通り、何が拒否されるか
このページのスキーマは、それを書き出す元になったサンプルを Zod 3 でも Zod 4 でも同じように受け付けます。例外は、専用の知らせがある一つの場合、つまり Zod 4 での無限大の数値だけです。そのサンプルを離れると、スキーマが述べる範囲に収まるものなら何でも受け付けます。実際に受け取るデータについては、次のようになります。
- 深さの上限に届かない範囲では、".optional()" なしで書かれたキーは必須です。そのキーを省いたペイロードは拒否され、スキーマが名指ししない種類の値をそこに置いたペイロードも拒否されます — "z.number()" とあるところの文字列や、"z.string()" とあるところのオブジェクトです。
- ".optional()" 付きで書かれたキーは欠けていてもよく、".nullable()" 付きで書かれたキーは null を保持してもかまいません。どちらもそれ以外の種類の値は通さないので、任意のキーが存在する場合は、やはりスキーマが名指しするものを保持していなければなりません。
- "z.union" はそのメンバーのどれかを受け付け、それ以外は受け付けません。"z.array" は、各要素が要素用に書かれたスキーマに合う限り、要素がいくつでも — 一つもなくても — 受け付けます。
- サンプルが何も示さなかったところ — 空の配列の要素、キーのないオブジェクト、深さの上限を超えてネストした値 — では、スキーマはそこにあるものを検査しません。どんな値でも受け付け、サンプルのオブジェクトにキーがなかったところではどんなオブジェクトでも受け付けます。その場所は知らせが示します。
スキーマが列挙していないキーは通されたうえで、結果から取り除かれます。これが取り除く動作で、それには専用の節があります。これらすべての外に立つキーが一つあります。"__proto__" と綴られたキーは、計算されたキーとして角括弧の中に書かれます。そのまま書くと、キーを名付けるのではなく、それが置かれたオブジェクトのプロトタイプを設定してしまうからです — それでも、どちらのバージョンの Zod も解析が返す結果にそのキーを含めず、Zod 4 はその値をまったく検査しません。
なぜスキーマは自ら厳密にならないのか
どのサンプルにも、それ自身には証明できない共通点があります。どの ID も整数、どのメールアドレスもメールアドレスの形、ロールには admin としか書かれていない、といったことです。それは規則性であり、規則性は検査ではありません。生成器はそれでも検査を書き込みたくなるものです — ID に ".int()"、アドレスに "z.email()"、ロールにリテラルを — が、この生成器は決してそうしません。サンプルが示すのはデータが保持しうるものであって、保持しなければならないものではないからです。データより厳密な型の代償は、手元の端末でのコンパイルエラーです。データより厳密なスキーマの代償は、本番環境で拒否されるリクエストです。admin ではない最初のロール、7.5 という最初の ID、パターンが予期しなかった最初のアドレスがそれです。
しかも、そうした検査はあなたの足元で変わります。Zod 4 の "z.uuid()" はバリアントビットを確かめるので、Zod 3 の ".uuid()" が受け付けていた、UUID の形をした文字列を拒否します。そして Zod 4 の ".int()" は、Zod 3 のものが通していた安全な範囲の外の整数を拒否します — ですから、今日のサンプルから推測した検査は、どちらの Zod をインストールするかで別の検査になります。そういうわけで、サンプルがほのめかすだけのことはスキーマに書き込まれません。データが届くときに問題になる箇所では、代わりにページが知らせで伝え、編集はあなたに任されます。
スキーマが自分では語らないこと
スキーマの下に、ページは気づいたものの書き込まなかったことを並べます。下の種類のうち当てはまるものごとに知らせが一つ、それが当てはまるすべての位置とともに一度だけ示されます。位置は出力が名前を付けるとおりに書かれます — キーなら "Customer.phone"、配列の要素なら "Root.tags[]"、識別子でないキーと "__proto__" ではキーを引用符と角括弧で囲んで — ので、スキーマの中で一目で見つけられます。ページを開いたときに読み込まれている例は、Infinity についてのもの以外のすべての種類を示します。
- null だけ。その位置でサンプルは null 以外を一度も保持しなかったので、スキーマは "z.null()" と書き、最初の本物の値を拒否します。フィールドに値が入るとき何を保持するかを決めてご自身で書くか — 例えば "z.string().nullable()" — 値の入ったサンプルを貼り付けてください。
- Infinity。1e999 のように JavaScript の数値で表せる範囲を超える数値は、JSON の解析時に Infinity または -Infinity になります。Zod 4 の "z.number()" は無限大の数値を拒否し、Zod 3 のものは受け付けるので、Zod 4 ではこれが、スキーマが自分の作られた元のサンプルそのものを拒否する唯一の場合です。これが投げかける問いは、スキーマではなくデータについてのものです。どのバリデータの目に触れるよりも前に桁は失われているので、その値がそもそも数値であるべきかどうかは、それを書き出した側が答えることです。
- 整数。その位置の数値はどれも整数で、"z.number()" は 7.5 も受け付けます。値が整数でなければならないところ — ID、個数、数量 — では ".int()" をご自身で追加し、たまたま端数がなかっただけの価格なら、そのままにしておいてください。"Number.MIN_SAFE_INTEGER" から "Number.MAX_SAFE_INTEGER" までの安全な範囲の外にある整数を保持する位置には、この知らせは出ません。Zod 4 の ".int()" がそうした整数を拒否するからで、スキーマに自身のサンプルを拒否させることになる助言だけは、ページが決してしない種類のものです。
- 推論するものがない。空の配列の要素、キーのないオブジェクト、そして 100 段階を超えてネストした値は、推論に何の手がかりも与えないので、スキーマはそこにあるものを検査しません。"z.unknown()" はどんな値でも受け付け、"z.record(z.string(), z.unknown())" はどんなオブジェクトでも受け付けます。その配列に要素があり、そのオブジェクトにキーがあるサンプルを貼り付けるか、スキーマのその部分を手で書いてください。
知らせがスキーマを一バイトでも変えることはありません。知らせはスキーマの横に、ページの言語で置かれる一文であり、それが指し示す編集をするかしないかはあなた次第です。文字列の形式、リテラル、enum についての知らせはありません。どれも、サンプルには示せない規則を推測することになるからです。
スキーマが列挙していないキーは取り除かれる
"z.object" は、列挙していないキーを持つオブジェクトを通し、返すものからはそれらのキーを除きます。これはどちらのバージョンでも Zod の既定の動作で、スキーマがすることのうち最も静かなことです。サンプルにたまたまなかったフィールドは、それを告げるエラーもないまま、コードが受け取るデータから消えます。このページのすべてのスキーマの下にある一文がそれに触れているのは、そのためです。
ページは、厳格にするか緩くするかをあなたの代わりに選びません。どちらも、サンプルが示せる以上のことを主張するからです。厳格なオブジェクトは列挙していないキーをすべて拒否します。ここにあるキー以外はない、ということですが、次のペイロードについてそれを証明できるサンプルはありません。緩いオブジェクトは余分なキーを残し、その推論された型にはそれらのキーのためのインデックスシグネチャが加わるので、TypeScript のページの答えではなくなってしまいます。取り除く動作は、TypeScript の型が受け付けるものを受け付け、それでいてその型を推論する唯一の動作です — 余分なプロパティを持つ値も、インターフェースを満たすのですから。別の選択をするには、スキーマを手で変更してください。
- スキーマが列挙していないキーを拒否するには、Zod 4 では出力が "z.object" と書いているところに "z.strictObject" と書き、Zod 3 では "z.object" に ".strict()" をチェーンします。
- それらのキーを残すには、Zod 4 では "z.looseObject" と書き、Zod 3 では ".passthrough()" をチェーンします。Zod 4 はこの二つの Zod 3 のメソッドを今も実行し、それらをレガシーと呼んでいます。
出力の各オブジェクトはそれぞれが一つのスキーマなので、選択はオブジェクトごとに行います。ルートを厳格にしても、その中にネストしたオブジェクトについては何も変わりません。いちばん外側の包みだけが自分で決めてよい部分であるときには、しばしばそれこそが望む形です。
スキーマとその型に一つの名前
各スキーマの後にはその型が続きます — "export const Customer" のすぐ後に "export type Customer = z.infer<typeof Customer>" — これは zod.dev が自身の例を書くときの書き方です。データを検査する値と、それが生む型とに一つの名前を使います。TypeScript は値と型を別々の名前空間に置くからです。その型は「JSON → TypeScript 変換」のページが同じ JSON に対して出力する型そのもので、名前も一つ残らず、キーも一つ残らず同じ、任意のキーも union も同じで、null も同じ場所にあります — 下に挙げる例外を除いて。
リポジトリは、その約束を信じるのではなく確かめています。サンプル集が両方のページを通り、続いて Zod の各バージョンとともに TypeScript のコンパイラを通ります。コンパイラは名前ごとに、二つの型が同一かどうか、それぞれが互いに代入可能かどうかを問われます。違う答えが返る箇所は、文書の深いところにあります。深さの上限を超え、キーが "z.unknown()" を保持するところでは、Zod 3 はそのキーを任意と推論しますが、TypeScript のページは必須にします。そして Zod 4 では、数十段階の深さにネストした配列の型をコンパイラが諦め、その型自身の行でエラー TS2589 を報告します。その行の上にあるスキーマはそれでも実行され、それでもサンプルを受け付けます。失われるのは推論された型だけです。
どちらの比較も、任意のキーを TypeScript が既定で読むとおりに読みます。プロジェクトが有効にしない限りオフになっている "exactOptionalPropertyTypes" のもとでは、任意のキーの "z.infer" は、どちらのバージョンの Zod でも明示的な undefined も認めますが、TypeScript のページの型は認めません。
Zod 4 向けに書かれ、Zod 3 でも有効なまま
出力は、二つのメジャーバージョンの両方にあるものだけを使います — "z.object"、"z.array"、二つ以上のメンバーにわたる "z.union"、"z.string()"、"z.number()"、"z.boolean()"、"z.null()"、"z.unknown()"、"z.record(z.string(), z.unknown())"、".optional()"、".nullable()"、そして "z.infer" — それも、zod.dev 自身の例が冒頭に置く import の行のもとでです。その中に Zod 4 で加わったものはなく、そこで非推奨になったものもないので、まだ Zod 3 から移っていないプロジェクトでもそのまま貼り付けられます。
ただし、同じテキストが両方でまったく同じに振る舞うわけではなく、それぞれの違いは、このガイドの中でそれが問題になる箇所で述べています。Zod 4 の "z.number()" は無限大の数値を拒否し、Zod 3 のものは受け付けます。これが Infinity の知らせです。値が "z.unknown()" のキーは、Zod 3 の推論された型では任意、Zod 4 の型では必須です — そして Zod 4.4 からはデータの解析時にも必須です — が、この出力がそれを書くのは深さの上限を超えたところだけです。Zod 3 は "__proto__" と綴られたキーを検査し、Zod 4 は検査しません。そして数十段階の深さにネストした配列については、コンパイラは Zod 4 の型を諦めますが、Zod 3 の型は導き出します。
これはメソッドを使う通常の Zod 向けに書かれていて、"z.string().nullable().optional()" のようになります。Zod Mini は同じスキーマを代わりに関数で "z.optional(z.nullable(z.string()))" と綴るので、Zod Mini はこの出力をそのままでは実行できません。
なぜルートが最後に来るのか
TypeScript のページはルートを最初に、それが使うオブジェクトをその後に出力します。型は、それを宣言する行より前で使えるからです。スキーマはそうはいきません。スキーマは値であり、自身の宣言より上で読まれた "const" は、モジュールの読み込み中に ReferenceError を投げます。そのためここでは、どのスキーマもそれが使う各スキーマの後に来て、ルートは最後に来ます — 例では Customer と LineItem が先で、それらを保持する Root がその後です — だから、TypeScript のページの冒頭に来るルートが、このページでは最後を締めくくるのです。
そうした順序は必ず存在します。推論は木構造で、抽出される各オブジェクトはちょうど一か所からだけ使われるので、自分自身や自分より後に出力されるスキーマを参照する必要のあるスキーマはなく、出力が "z.lazy" を必要とすることもありません。
よくある質問
- なぜ ID は "z.number().int()" ではなく "z.number()" なのですか?
- サンプルが示せるのは、これまでの ID がどれも整数だったことであって、次の ID も整数だということではないからです。代わりにページがそのことを伝えます。整数の知らせは、どの数値も整数だった位置をそれぞれ挙げます。値が整数のままでなければならないと分かっている箇所では、".int()" を足すのは一語だけの編集です。数値が安全な範囲の外の整数である位置では、この知らせは省かれます。Zod 4 の ".int()" はそのサンプル自体を拒否してしまうからです。
- なぜメールアドレスはただの "z.string()" として出てくるのですか?
- サンプルの中でメールアドレスのように見える文字列は次の文字列について何も語らないうえ、正しい形式の検査は Zod 自身のパターンに従わなければならず、そのパターンはバージョン間で変わるからです — Zod 4 の "z.uuid()" は、Zod 3 の ".uuid()" が通していた文字列をすでに拒否します。日付、URL、UUID が文字列のままなのも同じ理由で、いくつかの値しか持ったことのないフィールドが enum やリテラルになることもありません。規則が分かっているなら、書き込んでください。スキーマは、あなたが所有する普通のコードです。
- なぜ私のサンプルそのものが Zod 4 で失敗するのですか?
- JavaScript の数値で表せる範囲を超える数値 — 例えば 1e999 — を含んでいて、それが JSON の解析時に Infinity または -Infinity になり、Zod 4 の "z.number()" は無限大の数値を拒否する一方、Zod 3 のものは受け付けるからです。関わるすべての位置は Infinity の知らせが挙げます。それを除けば、スキーマは元になったサンプルを必ず受け付けます。サンプルのすべての値がスキーマに入っているからで、リポジトリはサンプル集を使い、両方のバージョンでそれを確かめています。
- 送ったフィールドが、なぜ解析結果から消えたのですか?
- スキーマがそれを列挙していないからです。"z.object" は、どちらのバージョンでも、余分なキーを持つオブジェクトを受け付けてから、それらのキーを除いて返します。サンプルになかったキーは、スキーマが一度も知ることのなかったキーです。スキーマにそのキーを追加するか、未知のキーを手つかずのまま通すべきなら、そのオブジェクト一つだけを緩くしてください — Zod 4 では "z.looseObject"、Zod 3 では ".passthrough()" です。
- スキーマを別のスキーマの下に移したら ReferenceError が出ました。なぜですか?
- スキーマは値であり、JavaScript は値を、それを定義する行より上で読ませてくれないからです。ページが各スキーマを、それが使うすべてのスキーマの後に、ルートを最後にして出力するのは、まさにそのためです。スキーマを、それを参照するすべてのものより上に置けば、エラーは消えます。
- なぜスキーマの名前は CustomerSchema ではなく Customer なのですか?
- それが zod.dev 自身の慣習です。スキーマと、それが推論する型は一つの名前を共有します。TypeScript では値と型が別々の名前空間にあるので、それが許されます。名前そのものは「JSON → TypeScript 変換」のページが同じオブジェクトに付ける名前なので、スキーマでもその型でも、両方のページで同じ語になります。
- 出力はどのバージョンの Zod 向けに書かれていますか?
- Zod 4 です。Zod 3 にないものは何も使っていないので、どちらでも変更なしに動きます。二つはいくつかの箇所で異なり — 無限大の数値、深さの上限を超えたキー、"__proto__" と綴られたキー、そしてとても深くネストした配列の型 — どれも上で説明しています。これはメソッドをチェーンする通常の Zod です。Zod Mini は同じスキーマを関数で綴るので、そのままでは実行できません。
- 認証情報も含めて、本物のデータを貼り付けてもかまいませんか?
- はい。スキーマはあなたのブラウザ内で組み立てられます。貼り付けた内容はあなた自身の端末で読み取られ、サーバーへの送信も、保存も、記録もされません。それにスキーマはあなたの値を一つも保持せず、キーと、それぞれの下にある値の種類だけを保持するので、サンプル中のトークンは "z.string()" として出てくるだけで、それ以上にはなりません。
関連するツール
- JSON → TypeScript 変換
このページのスキーマはコードの一部として実行され、データが届くたびにそれを検査します。あちらのページは同じ JSON に対して同じ答えを、同じ名前の素の TypeScript の型として出力します。型はコンパイル時に検査され、実行時には何も加えません。
- JSON → Go 変換
このページは、サンプルの数値がすべて整数だったところでも整数を必須にはせず、その検査を加えるかどうかは使う側に委ねます。あちらのページは同じ形を同じ型名で割り出しますが、Go には単なる JSON の数値にあたる型がないため、どの数値も整数として書かれているところではフィールドを整数型にし、小数点を付けて書かれた値はそこにデコードされません。
- JSONPath テスター
RFC 9535 の JSONPath クエリを JSON に対してテスト。
- Markdown テーブル生成
CSV・TSV・JSON から Markdown テーブルを生成して整形します。