JSON Schema to Zod
Turn a JSON Schema into Zod source with z.infer types — every dropped constraint named, entirely on this device.
The generated Zod source appears here as you type.
What the generator changed or dropped
Constraints such as pattern, minLength, minimum, multipleOf and uniqueItems have no equivalent in the generated schema — add the matching Zod checks by hand.
- Nothing converted yet — losses are listed here as you type.
Press ? for shortcuts
Do it offline:$ npx quicktype --lang typescript-zod --src-lang schema --top-level Root schema.json
How to use it
- Paste the schema, or drop a .json file on the left pane — the Zod source rebuilds as you type.
- Name the root type if Root is not what you want; every $defs entry keeps the name the generator gave it.
- Pick the import spelling: import * as z, or the named import some bundler setups prefer.
- Before you ship, read “What the generator changed or dropped” — one line per keyword that did not survive, and where.
Your JSON Schema is already the authority on what a payload may be. Paste it here and get Zod source you can drop in at the edge of a program, with a z.infer type beside each schema. Structure travels intact; most assertion keywords have no Zod spelling on this path and are listed, one per keyword, with the pointer where each one sat. Nothing is uploaded.
Examples
{
"$defs": {
"Role": { "enum": ["admin", "user"] }
},
"type": "object",
"properties": {
"id": { "type": "integer" },
"role": { "$ref": "#/$defs/Role" },
"nick": { "type": ["string", "null"] }
},
"required": ["id"]
}import * as z from "zod";
export const RoleSchema = z.enum([
"admin",
"user",
]);
export type Role = z.infer<typeof RoleSchema>;
export const RootSchema = z.object({
"id": z.number(),
"nick": z.union([z.null(), z.string()]).optional(),
"role": RoleSchema.optional(),
});
export type Root = z.infer<typeof RootSchema>;
Four schema features in one pass. The $defs entry becomes its own exported schema and is referenced, not copied; an all-string enum becomes z.enum; type [string, null] becomes a union with z.null in it; and only id is in required, so the other two carry .optional().
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email", "pattern": "^.+@.+$", "maxLength": 254 },
"age": { "type": "integer", "minimum": 0, "maximum": 130 },
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
},
"required": ["email"]
}import * as z from "zod";
export const RootSchema = z.object({
"age": z.number().optional(),
"email": z.string(),
"tags": z.array(z.string()).optional(),
});
export type Root = z.infer<typeof RootSchema>;
The honest half of the tool: six assertions went in and none came out, so the result accepts any string as an email, any number as an age. The dropped-keyword list names all six with their pointers, so restoring the checks is a checklist.
{
"type": "object",
"required": ["id", "createdAt"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"createdAt": { "type": "string", "format": "date-time" }
}
}import * as z from "zod";
export const RootSchema = z.object({
"createdAt": z.coerce.date(),
"id": z.string(),
});
export type Root = z.infer<typeof RootSchema>;
Exactly one format is understood, and it changes the value: "date-time" becomes z.coerce.date(), so parsing returns a Date object rather than the string that went in. "uuid" leaves a bare z.string() behind — worth knowing before an id is trusted.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["label"],
"properties": {
"label": { "type": "string" },
"next": { "$ref": "#" }
}
}import * as z from "zod";
export type Next = {
"label": string;
"next"?: Next;
};
export const NextSchema: z.ZodType<Next> = z.lazy(() =>
z.object({
"label": z.string(),
"next": NextSchema.optional(),
})
);
export const RootSchema = z.object({
"label": z.string(),
"next": NextSchema.optional(),
});
export type Root = z.infer<typeof RootSchema>;
The edge case worth seeing early. A schema that points at itself needs z.lazy plus a hand-written type annotation, because inference cannot close the loop — and the recursive half is named after the property that reached it, Next, not after the root.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Which keywords survive the trip
Structure survives: type, properties, required, items, enum, const, a local $ref, allOf, oneOf and anyOf. Of the assertion vocabulary only minItems and maxItems come through, as .min() and .max() — though the generator still lists those two as dropped, so trust the emitted code. The rest is reported and left to you.
Definition names are a hint, not a promise
A local definition is named after the first property that reaches it, so two properties sharing one definition yield one schema carrying the first name, and a definition key holding a space or a punctuation mark never reaches the output. Set the root name yourself; rename the rest after pasting.
Older drafts are read, not refused
draft-04 and draft-03 documents convert. Their structural vocabulary is understood, including the draft-03 habit of marking a property required with a boolean on the property itself. What differs between drafts is mostly assertion vocabulary, dropped on every draft alike.
Nothing is fetched, so nothing remote resolves
A $ref pointing at an http or https address stops the run before the generator starts, and the message names the pointer it sat at. Inline those definitions, or paste the resolved document. This is a privacy property first: a schema URL can identify an internal service.
Common errors
Unexpected '}' at line 1, column 19 — probable cause: trailing comma after the last property.- The document is parsed as JSON before anything else runs, so a typo is reported with a caret frame at the character rather than as a generator failure. Jump to error puts the cursor there.
This schema references "https://example.com/user.json" at #/properties/user — nothing is ever fetched here. Inline the referenced definitions (or paste the resolved schema) and run it again.- Remote references are detected first and the run stops. No request is attempted at any point, which is why a schema spread over several files must be resolved before it is pasted.
Code generation failed: Schema "false" is not supported at Root#- A boolean false where a schema is expected accepts nothing, so the generator refuses the whole document when it sits at the root. On one property it is survivable: that property is left out and the dropped-keyword list says so.
Input is larger than the 1 MB limit for this tool.- Schemas are documents, not payloads, so the ceiling sits far below the sample-driven pages'. Past it the run is refused whole — a half-converted schema is worse than none.
"pattern" has no equivalent in the generated schema and was dropped at #/properties/email/pattern — add the matching Zod check by hand.- One line per keyword, each carrying the JSON Pointer it was found at. These are advisories: the source still generates. Past twenty pointers the line says how many more went unlisted.
FAQ
Can I go the other way, from Zod back to a JSON Schema?
Not on this page. Reading Zod means parsing TypeScript source, a different machine from the one here, so this route converts in one direction only.
Why is my email field just z.string()?
Because format is an annotation this generator does not act on, apart from date-time. The dropped-keyword list names it with its pointer; add .email(), or your own regex, by hand.
Does the generated file work with the Zod version I have?
That is yours to check: this page never runs the file it writes. It can tell you exactly which API it emits — z.object, z.array, z.union, z.enum, z.coerce.date, z.lazy and z.infer — to compare against your version's documentation.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Convert again at once, without waiting out the typing pause.
- ⌘/Ctrl + S
- Save the generated source as a .ts file instead of saving the web page.
- ⌘/Ctrl + C
- With no selection live, puts the entire generated file on the clipboard.
- ⌘/Ctrl + Shift + C
- Take the whole file even when something on the page is selected.
- t
- Switch between the light and dark themes, except while a text field has focus.
- ? or ⌘/Ctrl + /
- Show the shortcut list with the glyphs your operating system uses.
- Esc
- Close whichever overlay is on top, leaving the schema untouched.