Zod schema generator from JSON
Validating external data — API responses, webhook payloads, config files — with Zod is one of the cleanest patterns in modern TypeScript, but hand-writing the schema for a deeply nested object is tedious and error-prone. This tool takes a real JSON sample and infers a matching Zod v3 schema, including nested objects, arrays, nulls, and a generated TypeScript type alias.
The typical workflow without this tool: copy the API response, stare at a deeply
nested structure, type out every z.object() by hand, miss a field, ship broken
validation. This generator short-circuits that cycle to a few seconds.
How it works
The generator recursively walks your parsed JSON. The mapping is straightforward:
| JSON value | Zod output |
|---|---|
"hello" | z.string() |
42 or 3.14 | z.number() |
true / false | z.boolean() |
null | .nullable() modifier added |
{ ... } | z.object({ ... }) (recursed) |
[...] | z.array(elementSchema) |
[] (empty) | z.array(z.unknown()) |
When you supply an array of objects at the top level, the tool unions keys across
all items: a key missing from some items becomes .optional(), and any field
that appears as null in at least one item becomes .nullable(). The final
output is a named schema constant plus a z.infer<typeof Schema> type alias you
can use directly.
Worked example
Paste this GitHub-style user payload:
{
"id": 12345,
"login": "alice",
"name": "Alice Smith",
"email": null,
"bio": "TypeScript enthusiast",
"public_repos": 42,
"site_admin": false
}
The tool produces something like:
import { z } from "zod";
export const Schema = z.object({
id: z.number(),
login: z.string(),
name: z.string(),
email: z.string().nullable(),
bio: z.string(),
public_repos: z.number(),
site_admin: z.boolean(),
});
export type Schema = z.infer<typeof Schema>;
From here you can call Schema.parse(response) to throw on invalid data, or
Schema.safeParse(response) to get a typed result/error union without throwing.
Adding refinements after generation
The generated schema is a structural skeleton, not the final production schema. Once the shape looks right, layer on value-level constraints:
z.string().email()— validate email formatz.string().uuid()— enforce UUID format on ID stringsz.number().int().min(0)— non-negative integersz.string().url()— URL stringsz.array(z.string()).min(1)— non-empty arrays.transform(val => new Date(val))— parse ISO date strings toDate
These additions are mechanical but often catch real bugs at the boundary between your code and an external service.
Common mistakes and edge cases
Single object versus array of objects. If you paste just one object, every field appears as required even if the API sometimes omits it. Paste a few representative payloads — or a JSON array of multiple responses — so the tool can detect which keys genuinely vary.
Discriminated unions. If your API returns different shapes based on a type
field (a common REST pattern), the generator will merge the shapes into one
z.object(). After generation, split it into a z.discriminatedUnion("type", [...]) manually — that gives Zod the information to give tighter error messages.
Dates as strings. JSON has no native date type, so "2024-01-15T08:30:00Z"
comes out as z.string(). Add .datetime() or .transform(s => new Date(s))
once you know the field is a date.
Empty arrays. A [] in your sample becomes z.array(z.unknown()) because
the element type is unknowable. Provide a sample with at least one element to
get a real element schema.
Why Zod over TypeScript interfaces alone
TypeScript types are erased at runtime. A fetch() response typed as an
interface gives you zero runtime protection — any malformed field passes
straight through. Zod validates and coerces at the boundary, so your application
code can trust that user.id is actually a number, not whatever the server
happened to return. The z.infer alias means you write the schema once and get
both the runtime check and the static type for free.
Tips and notes
- Give a full sample. Optionality is inferred from what is present. The more complete and representative your JSON, the more accurate the schema.
- Paste arrays for better optionality. An array of several objects lets the tool detect which fields are sometimes absent or null.
- Refine afterwards. Add
.email(),.min(),.uuid(), and other refinements once the structure is right — the tool gives you the skeleton. - Local only. Your payload never leaves the browser — safe to paste real API responses, tokens, or production data.