JSON to Zod schema
Paste JSON and get the z.object() that accepts it — one schema per nested object, .nullable() where the sample has null, the inferred type at the end. Zod 3 syntax, nothing to set.
What you get
- Writes the API-response sample as zod 3 · 7 schemas · 34 fields
- Puts every nested object in its own z.object(), children before the schema that names them, and the root last with export type X = z.infer<typeof X>
- .nullable() where the sample has null, .optional() where a key is missing from some elements — the paginated list marks failure_message optional
- .datetime() and .int() only when every value seen earns them: one plain string, or one number written with a point — 149.00 — and the field is z.string() or z.number()
- Names the root after the file — clean-order-example.json → CleanOrderExample — and a list’s element from its key: items → Item
What it will not do
- Write Zod 4, Valibot or io-ts. Zod 3 syntax — z.string().datetime(), z.number().int() — which Zod 4 still reads.
- Add .min(), .email(), .url() or an enum. A string is a string until you say otherwise.
- Guess a null’s type beyond string: a field that is only ever null is z.string().nullable(), said so here.
- Send your text anywhere. The page is static and the engine runs in this tab — open the network panel and watch it stay empty.
What you get
- An object → z.object(), named from the file
{"id": 1, "status": "paid"}import { z } from "zod"; export const Order = z.object({ id: z.number().int(), status: z.string(), }); export type Order = z.infer<typeof Order>;- A key missing in some items → .optional()
[{"id":1,"note":"x"},{"id":2}]import { z } from "zod"; export const OrderItem = z.object({ id: z.number().int(), note: z.string().optional(), }); export const Order = z.array(OrderItem); export type Order = z.infer<typeof Order>;- null in some items → .nullable()
[{"id":1,"ship":null},{"id":2,"ship":"air"}]import { z } from "zod"; export const OrderItem = z.object({ id: z.number().int(), ship: z.string().nullable(), }); export const Order = z.array(OrderItem); export type Order = z.infer<typeof Order>;- An RFC 3339 string → .datetime(), a number written whole → .int()
{"created_at": "2026-04-21T14:32:08Z", "qty": 2, "total": 149.00}import { z } from "zod"; export const Order = z.object({ created_at: z.string().datetime(), qty: z.number().int(), total: z.number(), }); export type Order = z.infer<typeof Order>;total stays z.number(): 149.00 was written with a point, whatever it parses to.
The sample
What the first sample opens — a real file, not an illustration written for this page.
in VS Code
Convert, then Zod — the schemas land in a new .ts editor beside the JSON.
Questions
Zod 3 or Zod 4?
Zod 3 syntax. Zod 4 reads z.string().datetime() and z.number().int() as written; only the deprecation notes differ.
Why is the root schema last?
A const has to be declared before another refers to it, so each z.object() lands after the ones it uses. The root names them all, so it comes last — and the file type-checks as written.
What happens when a field is only ever null?
It is z.string().nullable(). A lone null says nothing about the value, so this is the one guess the schema makes — and it guesses string.