JSON to Zod
The result appears here.
Generate
Your document never leaves the browser.
Zod schemas and their inferred types, from a JSON sample — in your browser.
TypeScript types vanish when the code runs, so they cannot stop a malformed response. A Zod schema can, and this page writes one from a sample, along with the types Zod infers from it.
Each object becomes an exported z.object constant, named after its type with Schema on the end. A schema must exist before another one uses it, so the nested ones come first: CustomerSchema and ItemSchema are written before the root. Whole numbers become z.number().int(), so qty rejects 2.5 while price accepts it.
note appears in every line item and is null in one of them, so it is z.string().nullable(). gift-wrap is absent from one, so it is z.boolean().optional(). The key keeps its quotes and its hyphen, because a Zod object’s keys are the document’s own and nothing needs renaming.
The sample, an order with two line items, pasted as the source
{
"order_id": 1042,
"placed_at": "2026-09-25T10:15:00Z",
"paid": true,
"customer": { "name": "Ada Lovelace", "email": "[email protected]" },
"items": [
{ "sku": "PEN-01", "qty": 2, "price": 3.5, "note": null },
{ "sku": "INK-07", "qty": 1, "price": 12, "note": "Fragile", "gift-wrap": true }
]
}The schemas written for it
import { z } from "zod";
export const CustomerSchema = z.object({
name: z.string(),
email: z.string(),
});
export const ItemSchema = z.object({
sku: z.string(),
qty: z.number().int(),
price: z.number(),
note: z.string().nullable(),
"gift-wrap": z.boolean().optional(),
});
export const RootSchema = z.object({
order_id: z.number().int(),
placed_at: z.string(),
paid: z.boolean(),
customer: CustomerSchema,
items: z.array(ItemSchema),
});
export type Customer = z.infer<typeof CustomerSchema>;
export type Item = z.infer<typeof ItemSchema>;
export type Root = z.infer<typeof RootSchema>;The sample, with import line off and z.infer type exports off
export const CustomerSchema = z.object({
name: z.string(),
email: z.string(),
});
export const ItemSchema = z.object({
sku: z.string(),
qty: z.number().int(),
price: z.number(),
note: z.string().nullable(),
"gift-wrap": z.boolean().optional(),
});
export const RootSchema = z.object({
order_id: z.number().int(),
placed_at: z.string(),
paid: z.boolean(),
customer: CustomerSchema,
items: z.array(ItemSchema),
});Option
import line: on by default, writing import { z } from "zod" at the top. Turn it off when the file already imports it.
Option
z.infer type exports: on by default, one export type per schema, so the types and the validators cannot drift apart. Turn it off if you keep your types elsewhere.
Call RootSchema.parse on the parsed response and you get either typed data or a ZodError listing every path that failed. safeParse does the same without throwing, returning success and either the data or the error.
Two objects that share the same keys and types, anywhere in the document, share one schema constant too. The schemas are strict about types and loose about extra keys, which is Zod’s own default: a key the sample never had is stripped from the result, not rejected. Add .strict() to an object where an unknown key should be an error, and refine strings with .email() or .datetime() where your data promises one.
Two addresses of one shape
{
"from": { "city": "Paris" },
"to": { "city": "Lyon" }
}One schema constant for both
import { z } from "zod";
export const FromSchema = z.object({
city: z.string(),
});
export const RootSchema = z.object({
from: FromSchema,
to: FromSchema,
});
export type From = z.infer<typeof FromSchema>;
export type Root = z.infer<typeof RootSchema>;Inference and code generation both run inside this browser tab. The sample is never uploaded, stored on a server or logged, because the page has no server to send it to, and once loaded it carries on working with the connection off.
The output uses the core API that Zod 3 and Zod 4 share: z.object, z.array, z.union, nullable and optional.
The shapes are the same; the difference is that Zod checks them at run time. The exported types match what the TypeScript page would write.
It is z.string() here, since JSON has no date type. Change it to z.string().datetime() to check the format, or add .transform to turn it into a Date.