Home JSON to Zod
JSON to Zod schema generator
Paste a JSON array and get the Zod schema that validates it — nesting kept, optional keys marked, and string formats recognised where Zod can check them.
Why it reads every record, not the first one
The usual failure of a JSON-to-type tool is that it looks at one object.
The first record's null becomes the type null; a key that
happens to be absent from it never appears at all; a field that is 7 in the
first row and 7.5 in the fortieth is typed as an integer. Each of those
compiles, passes review and breaks on the second page of results.
This reads every record you paste and merges what it finds:
- A key missing from any record is optional — the marker is on the key, not the value.
- A key that is sometimes null is nullable — a different statement from optional, and both can be true.
- Mixed numbers collapse to one number type. A field holding 7 and 9.5 is a number, not "an integer or a number".
- Recognised string shapes keep their meaning where the target can express it — UUIDs, ISO timestamps, dates, emails and URLs.
- An empty array says nothing about its elements, so it stays unknown rather than being guessed from a sibling record.
So paste more than one record. Paste the awkward ones — the row with the null, the row missing the optional field, the one from the second page. The output is only as good as the range of the sample, and that is a property of the input, not of the tool.
String formats become refinements
Zod can check more than "this is a string", so where every value in a field matches a recognised shape the generated schema says so:
| Every value looks like | Emitted as |
|---|---|
| a UUID | z.string().uuid() |
| an ISO 8601 timestamp | z.string().datetime() |
| an email address | z.string().email() |
| an http or https URL | z.string().url() |
| a whole number | z.number().int() |
Every value, not most — one value that does not match drops the refinement, because a validator that rejects your own data is worse than one that checks less.
Declaration order is not cosmetic
Nested schemas are emitted before the schema that references them. A const
used above its own declaration is a ReferenceError the moment the module is
imported — not a type error, a crash — so the file is written innermost-first even though
that puts the schema you came for at the bottom.
Getting the type back out
The generated file ends with an array schema for the whole payload. Add
z.infer where you need the static type, so the schema stays the single source
of truth rather than being kept in step with a hand-written interface:
export type User = z.infer<typeof userSchema>; export type UserList = z.infer<typeof userListSchema>;
If you only want the static type and no runtime validation, the TypeScript generator emits interfaces directly.
Common questions
Which Zod version does the output target?
The output uses z.object, z.array, z.union, .optional(), .nullable() and the string refinements .uuid(), .datetime(), .email() and .url() — all of which are present in Zod 3 and Zod 4.
Why are nested schemas defined before the one that uses them?
Because a const referenced above its declaration throws a ReferenceError when the module is imported. Order is correctness here, not style, so the file reads innermost-first.
When does it add .email() or .uuid()?
Only when every value in that field matches the shape. One value that does not drops the refinement, because a validator that rejects your own data is worse than one that checks less.
How do I get a TypeScript type from the schema?
Add z.infer<typeof schemaName>. Keeping the schema as the source of truth is the point — a hand-written interface next to a schema is two things to keep in step.
Does the JSON get uploaded?
No. Inference and generation happen in your browser.
Related
- /json-to-typescript — the static type without the runtime validator.
- /json-to-pydantic — the same job on the Python side.
- /json-to-json-schema — a validator description that is not tied to a language.
- /api-mock-data — generating fixtures that satisfy the schema you just made.
Last updated