Conversion guide
JSON to Zod: Generate Runtime Schemas
Generate a Zod schema from a JSON sample for runtime validation, plus an inferred TypeScript type. Handles unions, nullables, and optional fields.
TypeScript types prove things at compile time. When JSON crosses a network boundary, you also need runtime checks — otherwise a backend change silently feeds your frontend garbage. Zod is the most popular TypeScript-first validator, and generating a Zod schema from a JSON sample saves the tedious step of hand-writing one. This guide walks through the mapping.
Paste a sample into the JSON to Zod generator for instant output, or read on for how the conversion works and where it needs help.
TL;DR
A JSON to Zod converter mirrors the JSON value tree: objects become
z.object({ ... }), arrays become z.array(...), primitives map directly.
Multiple samples widen fields into .optional() and .nullable() correctly.
Pair the schema with z.infer<typeof Schema> to get the matching TypeScript
type for free.
Step 1 — Primitives and flat objects
{
"id": "u_001",
"name": "Otter",
"email": "otter@example.com",
"isAdmin": false
}
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
isAdmin: z.boolean(),
});
export type User = z.infer<typeof UserSchema>;
z.string() validates that the value is a string at runtime. .email() could
tighten it further if you know the field is always an email — that's a manual
refinement the generator skips by default.
Step 2 — Nested objects
Nested JSON becomes nested z.object calls. Extract them as named schemas for
reuse.
{
"id": "ord_A100",
"customer": {
"id": "u_001",
"name": "Otter"
},
"shipping": {
"city": "Springfield",
"zip": "00000"
}
}
const CustomerSchema = z.object({
id: z.string(),
name: z.string(),
});
const ShippingSchema = z.object({
city: z.string(),
zip: z.string(),
});
export const OrderSchema = z.object({
id: z.string(),
customer: CustomerSchema,
shipping: ShippingSchema,
});
export type Order = z.infer<typeof OrderSchema>;
Step 3 — Arrays
{
"tags": ["new", "discounted"],
"items": [
{ "sku": "widget", "qty": 2 },
{ "sku": "gadget", "qty": 5 }
]
}
const ItemSchema = z.object({
sku: z.string(),
qty: z.number(),
});
export const PayloadSchema = z.object({
tags: z.array(z.string()),
items: z.array(ItemSchema),
});
For mixed-type arrays, the generator emits z.array(z.union([...])) with one
branch per observed type. Clean the source data if that union surprises you.
Step 4 — Optional and nullable from multiple samples
Feed several samples and the generator distinguishes "missing field" from "field present but null".
Input (three samples)
[
{ "id": "u_001", "email": "otter@example.com" },
{ "id": "u_002", "email": null },
{ "id": "u_003", "email": "lynx@example.com", "verifiedAt": "2026-01-15" }
]
Output
export const UserSchema = z.object({
id: z.string(),
email: z.string().nullable(),
verifiedAt: z.string().optional(),
});
export type User = z.infer<typeof UserSchema>;
If you only have one sample, every field is required and non-nullable — feed more samples whenever you can.
Step 5 — Parse, don't just type-cast
Once the schema is in place, parse incoming JSON instead of casting it. Two patterns:
// Throw on invalid input — fine for boundaries you control.
const order = OrderSchema.parse(rawJson);
// Get a result object — better for user-facing errors.
const result = OrderSchema.safeParse(rawJson);
if (!result.success) {
// result.error.issues is a structured list of every problem.
return showError(result.error.issues);
}
const order = result.data;
safeParse pairs naturally with form validation, API ingest, and any place a
500 would be the wrong response to a malformed payload. If you also want a
compile-time-only contract somewhere upstream, generate a
TypeScript interface from the same sample.
Common conversion problems
"Zod says the value is invalid but it looks fine"
Print the raw value. Common culprits: trailing whitespace, an unexpected null,
or a stringified number ("42" not 42). The Zod error has a path field
pointing at the bad node.
"I want the schema to accept dates as strings or Date objects"
Use z.union([z.string().datetime(), z.date()]) and refine to a Date with
.transform. The generator emits z.string() by default because that's what's
in the JSON sample.
"My JSON has snake_case keys but my app uses camelCase"
Define the schema with snake_case keys (matching the wire format) and add a
.transform that maps to camelCase. Don't try to make the schema speak both —
pick one shape per layer.
"How do I tighten string formats?"
Add the refinement by hand: z.string().email(), z.string().uuid(),
z.string().min(1). The generator can't tell from the sample alone that a
string is, say, an email — it preserves only what the JSON proves.
"The output won't compile in my app"
Check your Zod version. The generated code uses the modern Zod 3 API. If you're
on Zod 1 or 2, some methods (.datetime, .brand) won't exist.
Generate in the browser, no upload
The free JSON to Zod generator on JSONZen emits a complete Zod schema plus the inferred TypeScript type. It runs entirely client-side, handles nested objects and arrays, and never sends your data anywhere.
For a complete contract, also generate the matching JSON Schema — useful when other consumers (non-TypeScript services) need to validate the same payload.
Closing recommendation
Generate the Zod schema at every network boundary in your TypeScript app: HTTP
responses, message-queue messages, third-party webhooks. safeParse at the
edge, typed values inside — that's the line between code that survives an API
change and code that crashes the moment one ships.
Related guides
- JSON to TypeScript: Generate Interfaces
Generate accurate TypeScript interfaces from a JSON sample. Handles nested objects, unions, optional fields, and nullable values without hand-typing.
- JSON Schema vs TypeScript Types: When to Use Which
JSON Schema vs TypeScript types compared on runtime vs compile-time, portability, validation power, and ecosystem. With concrete examples and recommendations.