JSON to Zod: Generate a Zod Schema From a JSON Sample
Generate a Zod schema from JSON online — optional fields and unions inferred correctly from multiple samples, Zod 4 and Zod 3 output. Free, client-side, no sign-up.
Try it now: JSON to Zod Schema — Generate a Zod schema from a JSON sample, with string formats and integers inferred only when every sample agrees. Zod 4 and 3 output.
What Zod Is, and Why Generate a Schema From JSON
Zod is a TypeScript-first schema library that validates data at runtime — it's what stands between a fetchresponse, a webhook payload, or a form submission and the rest of your code actually trusting that data's shape. Writing a Zod schema by hand from a real JSON sample is the same tedious, error-prone process as writing a TypeScript interface by hand: fine for a small flat object, slow and easy to get subtly wrong the moment there's nesting, arrays, or a field that's missing on some records. A generator that reads the sample and produces the schema removes the typing and, more importantly, removes the guessing about which fields are actually optional.
The output is a real Zod schema, not a description of one — UserSchema.parse(data) either returns typed data or throws with a precise field-level error, at runtime, which is the entire point of reaching for Zod instead of a TypeScript interface in the first place.
What a Generator Can Safely Infer — and What It Can't
A JSON sample only tells you what the data looked like, not what it's allowed to look like — and a generator that respects that distinction is more useful than one that guesses. It can tell a field is a number; it cannot tell you that number should be z.number().positive(), or capped at some maximum, or that a string field is actually constrained to five known values. Those are business rules, not structural facts, and inferring them from one sample is how you end up with a schema that rejects perfectly valid data the first time a value falls outside whatever range happened to appear in the example. A trustworthy generator stays conservative: it infers structure — object shape, array element types, nullability — and leaves constraints for you to add deliberately.
Where a generator cango further, safely, is string format and numeric precision — but only when every sample agrees. If a field's value looks like an email address in every record you pasted, it's reasonable to emit z.string().email() instead of a bare z.string(). If a numeric field never has a fractional part across the whole sample, z.number().int() is a defensible inference. But the moment one record breaks the pattern — one idfield that isn't a valid UUID, one age that's 30.5 — the safe move is to fall back to the loosest type that actually fits all the data, not to keep the tighter constraint and hope the outlier was a fluke.
{
"id": "usr_8f2b",
"email": "[email protected]",
"age": 36,
"active": true,
"roles": ["admin", "billing"]
}import { z } from "zod";
export const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number().int(),
active: z.boolean(),
roles: z.array(z.string()),
});
export type User = z.infer<typeof UserSchema>;emailgets the format check because the sample value parses as a valid email and there's only one sample to check it against. id stays a plain string — usr_8f2bisn't a UUID, an email, or any other format Zod ships a built-in check for, so tagging it as anything more specific than z.string() would be a fabricated constraint, not an inference.
Merging Multiple Samples: Optional Fields and Unions
The hard problem — and the detail that separates a genuinely useful generate zod schema tool from a shallow one — shows up only once you paste more than one record. A single JSON object gives a generator nothing to compare against; every field looks required and every type looks fixed, because there's only one data point. Paste an array of real records instead, and two situations come up constantly: a field present in some records but absent in others, and a field whose value type genuinely differs between records. Handled correctly, the first becomes .optional() and the second becomes a union. Handled by reading only the first record and assuming the rest match, both become silent bugs — a schema that rejects valid data missing an optional field, or one that throws on a value type the field legitimately takes.
[
{ "id": 1, "total": 9.99, "note": "gift" },
{ "id": 2, "total": 12.5 }
]export const OrderSchema = z.object({
id: z.number(),
total: z.number(),
note: z.string().optional(),
});id and total appear in every record, so they stay required. note appears in only one, so it's marked optional instead of required — a required note would make OrderSchema.parse()throw on every real order that doesn't include a gift note, which is most of them.
The same merge logic handles a field whose type outright changes between records:
[{ "v": 1 }, { "v": "x" }, { "v": null }]export const RowSchema = z.object({
v: z.union([z.number(), z.string(), z.null()]),
});A generator that only reads the first element would produce v: z.number() and reject the second and third records outright at validation time — exactly the kind of false negative that makes a runtime validator worse than no validator at all, because it fails on data that was always valid.
Zod 3 vs Zod 4 Output
Zod's major versions haven't kept the same API surface, and a generator that only knows one of them will hand you code that doesn't compile against whatever's installed in your project. Zod 4 moved several string-format checks — email, uuid, url, and others — out from under z.string() and into top-level functions, so z.string().email() in Zod 3 becomes z.email()in Zod 4. Error-message customization also shifted shape between the two majors. None of this is exotic — it's the normal cost of a library's syntax evolving across major versions — but it means a json to zod schema generator that hard-codes one version's syntax will quietly break for anyone on the other. The practical fix is letting the output target be a choice, not an assumption: pick Zod 3 or Zod 4 output based on what's in your package.json, generate against that, and move on.
TypeScript Interfaces vs Zod: Compile-Time Types vs Runtime Validation
It's worth being precise about when you want each, because they solve different problems from the same starting point. A TypeScript interface generated from a JSON sample — see the JSON to TypeScript guide — only checks shapes at compile time; the type annotation is erased entirely once the code is compiled, and it does nothing to stop malformed data from an actual API call or file upload at runtime. A Zod schema checks the shape while your program is running, on the actual value in hand, and can produce a TypeScript type for free via z.infer<typeof Schema>— so you get the compile-time type and the runtime guard from a single source of truth instead of maintaining both by hand and having them drift apart. Use an interface when you control both ends of the data (your own internal function signatures); use Zod at any boundary where the data comes from somewhere you don't fully trust — a third-party API, a webhook, user input, an environment variable.
- Validating an external API response before using it. Parse the response through the generated schema before touching any field — a schema mismatch surfaces immediately as a thrown error with the exact field and reason, instead of an
undefinedsilently propagating three functions deep. - Validating a webhook payload.Webhook senders occasionally change their payload shape without warning; a Zod schema at the entry point catches the change the moment it happens rather than after it's corrupted downstream data.
- Form and request-body validation in a server action or API route.The same schema that describes your data's shape doubles as the validation layer, with no separate library needed and no hand-maintained duplicate of the type.
- Keeping a schema and TypeScript type from drifting apart. Deriving the type from the schema with
z.infermeans there is exactly one place the shape is defined — updating the schema updates the type automatically, which a separately hand-written interface can never guarantee.
GenKitLab's JSON to Zod generates a Zod schema from a JSON sample this way: string formats and integers are inferred only when every sample agrees, multiple records are merged into optional fields and unions rather than read from the first one, and you choose Zod 4 or Zod 3 output to match what's already in your project. It runs entirely client-side — nothing you paste is uploaded anywhere. Need the plain TypeScript type instead of a runtime schema, from the same kind of sample? JSON to TypeScript uses the same merge logic for optional fields and unions.
Frequently asked questions
›How do I generate a Zod schema from a JSON sample?
Paste the JSON — ideally an array of several real records rather than a single object — and a generator infers the shape: object fields become z.object() properties, arrays get an element schema, and fields get marked .optional() or wrapped in z.union() based on how the samples actually differ from each other.
›Can a JSON to Zod generator infer things like z.number().positive() or min/max?
Not safely, and a good generator won't try. A single JSON sample only shows what a value happened to be, not what it's constrained to be — inferring a range or a positivity check from one example risks producing a schema that rejects perfectly valid data the first time a real value falls outside that example's range. Structural inference (object shape, optional fields, unions) is safe; business-rule constraints are something you add deliberately after generating the base schema.
›What happens if a field is missing from some of my JSON records?
It becomes .optional() rather than being silently dropped or left required. A generator that reads only the first record in an array and assumes every other record matches will produce a schema that throws on real, valid data the moment a later record is missing that field.
›What if the same field has different types across my sample records?
It becomes a Zod union — for example z.union([z.number(), z.string(), z.null()]) — covering every type actually observed across the samples, rather than the generator picking the first record's type and rejecting the rest.
›Should I generate Zod 3 or Zod 4 syntax?
Whichever major version is installed in your project — check your package.json. Zod 4 moved several string-format checks (email, uuid, url, and others) from chained methods on z.string() to top-level functions like z.email(), among other syntax changes, so code generated for one major version won't compile cleanly against the other.
›Is a Zod schema better than a TypeScript interface for JSON data?
They solve different problems. A TypeScript interface only checks shapes at compile time and is erased entirely once your code is compiled — it does nothing once the program is actually running. A Zod schema validates the real value at runtime and can also produce a TypeScript type via z.infer, so it's the better choice at any boundary where the data comes from somewhere you don't fully control, like an external API, a webhook, or user input.
Last updated