JSON to Zod
JSON to Zod generates a runnable Zod schema from a sample document in your browser. Unlike a TypeScript interface, the output is code that runs: it parses a value at runtime and rejects what the sample did not contain. Paste several records so optional and nullable fields are inferred rather than guessed.
Empty JSON input (truncated document)
^
How does JSON to Zod work?
A TypeScript interface and a Zod schema look similar and do completely different jobs. The interface is erased when the code compiles: it tells your editor what the shape should be, and tells the running program nothing. A Zod schema is a value that survives into runtime, so it can parse an actual response and reject it. That is the difference between hoping the API kept its promise and checking that it did.
The same inference, a different rendering
This tool shares its type inference with the five language generators beside it — one walk of the sample decides which fields are optional, which are nullable, which object shapes are identical, and what the element type of a mixed array is. Only the rendering differs. That matters because those decisions are the hard part, and a second implementation would eventually disagree with the first.
Optional and nullable are two different things
{"a": null} and {} are different documents. Zod spells them differently too — .nullable() for the first and .optional() for the second — and neither implies the other. A key that is sometimes absent and sometimes null needs both, in that order: z.string().optional().nullable(). Getting this wrong is the usual reason a generated validator passes your fixtures and fails on the second week of production.
Format checks are a claim about every value
The inference notices when a string looks like a UUID, an email or a date-time and can emit z.uuid(), z.email() or z.iso.datetime() instead of a bare z.string(). That is genuinely useful and slightly dangerous: it is a claim about every value the field will ever hold, inferred from one sample. If a column is a UUID in your test fixture and a slug in production, the validator will reject real data. It is a toggle for that reason, and the reference table below says which validator each detected format maps to.
Zod 3 and Zod 4 spell formats differently
Zod 4 moved the string format validators to top level: z.uuid(), z.email(), z.url() and z.iso.datetime(), replacing the deprecated z.string().uuid() family. Both still run, so the target is a choice rather than a guess, and the default is Zod 4 because a schema generated today is going into a codebase starting today.
JSON sample
{
"id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"email": "[email protected]",
"score": 148,
"deleted_at": null,
"tags": ["priority", "gift"]
}Zod schema
export const RootSchema = z.object({
id: z.uuid(),
email: z.email(),
score: z.number().int(),
deleted_at: z.unknown(),
tags: z.array(z.string()),
});
export type Root = z.infer<typeof RootSchema>;What does a Zod schema generated from JSON look like?
A detected format becomes a real validator: the id looked like a UUID and the email like an email, so both are checked rather than merely typed. A value that was null in every sample becomes z.unknown() — accurate about the sample and honest about the data.
JSON sample
{
"id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"email": "[email protected]",
"score": 148,
"deleted_at": null,
"tags": ["priority", "gift"]
}Zod schema
import { z } from "zod";
export const RootSchema = z.object({
id: z.uuid(),
email: z.email(),
score: z.number().int(),
deleted_at: z.unknown(),
tags: z.array(z.string()),
});
export type Root = z.infer<typeof RootSchema>;
This is Zod 4 output: the format validators are top-level calls. Target Zod 3 instead and they become z.string().uuid() and z.string().email(), which Zod 4 still accepts but marks deprecated.
What options and edge cases does JSON to Zod support?
| Parameter | Type | Default | Behaviour & edge cases |
|---|---|---|---|
| Zod version | Zod 3 | Zod 4 | Zod 4 | Zod 4 uses top-level z.uuid(), z.email(), z.url() and z.iso.datetime(); Zod 3 uses the z.string().uuid() family, which Zod 4 still accepts but marks deprecated. Everything else in the output is identical. |
| strict objects | boolean | off | Adds .strict(), so a key the schema does not know about is an error rather than being silently dropped. Worth turning on for a request body you are validating; usually wrong for a third-party response that adds fields without telling you. |
| z.infer types | boolean | on | Emits export type X = z.infer<typeof XSchema> beside each schema, so the TypeScript type and the runtime validator cannot drift apart. This is the one place Zod is strictly better than a hand-written interface. |
| format checks | boolean | on | Applies z.uuid(), z.email(), z.url(), z.iso.datetime() and z.iso.date() where the sample looked like one. A single-sample inference is a strong claim — turn it off when a column is not homogeneous in production. |
| Optionality | inferred | across records | A key absent from at least one record of the same shape gets .optional(). Paste several records; one record cannot express this, and a schema built from one record will reject the second. |
| Nullable | inferred | .nullable() | A value that is null in some records and a string in others becomes z.string().nullable(). A key that is both sometimes absent and sometimes null gets .optional().nullable(), which is two different requirements and not a redundancy. |
| Integer | inferred | z.number().int() | A JSON number with no fractional part becomes an integer schema, so a float arriving where the sample had a whole number is rejected rather than quietly accepted. A field that is 1 in one record and 1.5 in another becomes a plain z.number(). |
| Root array | document shape | z.array(RootSchema) | A top-level array of records emits the element schema plus a RootSchema wrapper, so the document's own schema is one line rather than something you have to assemble yourself. |
| Input size | bytes | 2 MB | Inference reads every value. A few hundred representative records describe a shape as well as a million, so the ceiling costs nothing in practice. |
Frequently asked questions
Why generate a Zod schema instead of a TypeScript interface?
Because a TypeScript type is erased at compile time. It tells your editor what the shape should be and tells the running program nothing, so a response that violates it sails straight through. A Zod schema is a real value that runs at runtime: safeParse returns either the typed data or the exact paths that failed. If the API is yours and well tested, an interface is enough. If it is somebody else's, or user input, a validator is the only thing that actually checks.
Why does my field have .optional() when the API always sends it?
Because at least one record in the sample did not have it. The generator can only describe the data it was given. Paste a sample where the field always appears to get a required schema, or delete the .optional() by hand — which is the right call when you know the contract better than the sample does. Note that this is the opposite failure from .nullable(): an absent key and a null value are different, and the schema distinguishes them.
Is .nullable() the same as .optional()?
No, and conflating them is the most common bug in generated schemas. {a: null} has the key with a null value; {} does not have the key at all. .nullable() accepts the first, .optional() accepts the second, and a field that can be either needs both — z.string().optional().nullable(). Order matters for readability rather than behaviour, but Zod's own documentation writes optional first.
Why did a plain string become z.uuid() or z.email()?
Because every value the inference saw at that position matched the pattern, so it treated the format as part of the type. That is usually right and occasionally wrong: a column that holds UUIDs in your fixture and slugs in production will now fail validation on real data. Turn format checks off to get a bare z.string() everywhere, or edit the one line. The reference table lists exactly which format maps to which validator.
Which Zod version should I target?
Zod 4 unless you are pinned to Zod 3. Zod 4 moved the format validators to top level — z.uuid() instead of z.string().uuid() — and deprecated the old spellings, so a schema generated for Zod 4 has no deprecation warnings in a new codebase. The old calls still work in Zod 4, which is why a Zod 3 schema pasted into a Zod 4 project is not broken, merely dated.
Is my JSON uploaded anywhere?
No. Inference and rendering both run in the tab you already have open — open DevTools, watch the Network panel and paste a document. This matters more here than on most tools, because the documents people turn into validators are production API responses, frequently with real customer records in them.
Which related tools should I use next?
- JSON to TypeScriptGenerate interfaces from a sample, optionality and all.
- JSON Schema GeneratorInfer a Draft 2020-12 schema from samples and validate against it.
- JSON ValidatorValidate syntax with exact line and column, and repair it in one click.
- JSON ParserParse a document and see what it actually contains.