Guide
How to convert JSON to a Zod schema
A 5-minute, copy-paste-friendly walkthrough: from a raw JSON sample to a schema you can use for Zod validation, TypeScript inference, forms, and tRPC.
Need it now? Skip ahead to the JSON → Zod converter.
Step 1 — Get a representative JSON sample
Copy a real response from the network tab, a webhook fixture, or your test data. The schema is only as accurate as the sample you start from, so prefer a payload that includes optional fields and arrays you care about.
{
"id": 42,
"name": "Ada Lovelace",
"email": "ada@example.com",
"isAdmin": false,
"tags": ["math", "engine"]
}Step 2 — Generate the schema
Paste the sample into the JSON → Zod converter. You'll get something like this:
import { z } from "zod";
export const User = z.object({
"id": z.number().int(),
"name": z.string(),
"email": z.string().optional(),
"isAdmin": z.boolean(),
"tags": z.array(z.string()),
});
export type User = z.infer<typeof User>;Note the last line: z.infer<typeof User> gives you a TypeScript type derived from the schema, so the type and the runtime validator can never drift apart.
Step 3 — Refine field constraints
The generator infers types but not business rules. Tighten the schema with Zod's built-in refinements:
import { z } from "zod";
export const User = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
email: z.string().email().optional(),
isAdmin: z.boolean(),
tags: z.array(z.string()).default([]),
});
export type User = z.infer<typeof User>;- •
.email()— validate email format - •
.min(1)/.max(100)— enforce string length bounds - •
.positive()/.int()— number constraints - •
.default([])— provide a value when the field is missing - •
.refine(v => ..., "message")— custom rules
Step 4 — Add Zod validation at trust boundaries
The biggest win from a Zod schema is catching shape changes the moment they cross a boundary you do not fully control. Use .parse() when you expect valid data and want to fail loudly, or .safeParse() when you want to handle the failure inline.
import { User } from "./schemas";
export async function fetchUser(): Promise<User> {
const res = await fetch("/api/me");
const data = await res.json();
return User.parse(data); // throws ZodError on shape mismatch
}Zod validation checklist
- • Validate values from HTTP APIs, webhooks, forms, env vars, localStorage, and message queues.
- • Skip validation for values that already came from your own typed functions.
- • Prefer
safeParse()when the user can recover from bad input. - • Prefer
parse()when bad input should stop the request immediately.
Step 5 — Reuse the schema in forms
The same schema can drive client-side form validation. With @hookform/resolvers/zod the integration is two lines:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { User } from "./schemas";
export function UserForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(User),
});
return (
<form onSubmit={handleSubmit((v) => console.log(v))}>
<input {...register("name")} />
{errors.name && <span>{errors.name.message}</span>}
</form>
);
}Step 6 — Reuse in tRPC / API contracts
If you ship tRPC, pass the schema (or a transformed copy) directly as the procedure input. Same shape, same validator, same TypeScript types — end-to-end:
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
export const userRouter = router({
create: publicProcedure
.input(User.omit({ id: true })) // reuse the schema, drop the server-generated id
.mutation(async ({ input }) => {
// input is fully typed and runtime-validated
return db.user.create({ data: input });
}),
});Common pitfalls
- • Optionality is sample-dependent. A field is marked
.optional()only if its value was null in the sample. Real APIs often have more optional fields than one sample reveals. - • Integer vs number. Zod's
.int()is added when every numeric value in the sample was an integer. APIs that occasionally return floats will fail validation — drop.int()if unsure. - • Empty arrays lose element type. An empty array becomes
z.array(z.unknown()). Provide a non-empty sample to get a useful element type. - • Don't over-validate. Validating an internal value that's already typed is a code smell. Use Zod at trust boundaries: HTTP responses, form inputs, env vars, message queue payloads.
Troubleshooting generated Zod schemas
A generated schema is a strong first pass, but runtime validation gets strict quickly. These are the places developers most often need to edit the output before shipping it.
Optional vs nullable fields
JSON samples only show the value you pasted. If a field appears as null, the generator can mark it nullable, but it cannot know whether the API may also omit that field.
// Generated from a sample where email was null email: z.string().nullable() // Use this when the API may omit email entirely email: z.string().email().optional() // Use this when the API may send null or omit the field email: z.string().email().nullable().optional()
Numbers that arrive as strings
Browser forms, query strings, CSV imports, and some APIs send numbers as strings. Use coercion only at input boundaries where you expect that shape; keep internal data strict.
const User = z.object({
// Useful when a form field sends "42" as a string
id: z.coerce.number().int().positive(),
// Keep API timestamps as strings unless your app needs Date objects
createdAt: z.string().datetime(),
});Empty arrays and mixed arrays
Empty arrays usually become z.array(z.unknown()) because there is no element to inspect. Mixed arrays may also need manual review: decide whether the API really allows multiple shapes or whether your sample is combining unrelated data.
Dates, IDs, and enums
JSON has strings, numbers, booleans, arrays, objects, and null. It does not have native dates, UUIDs, or enums. Add .datetime(), .uuid(), or z.enum([...]) after generation when your domain requires tighter validation.
FAQ
No. The generator produces a working schema you can drop in. Reading the Zod docs only matters when you want to add custom rules.
Optionality is inferred from the sample. Review the output and add .optional() where appropriate.
Use Zod when you need runtime validation; use plain TypeScript when the value comes from a statically-typed source you already trust.
Yes — use JSON Schema → Zod or OpenAPI → Zod.
.optional() means the field may be missing or undefined. .nullable() means the field may be explicitly null. Some API responses need both.
Usually because the sample has an empty array or a mixed value that cannot prove a stable type. Paste a more representative sample, then tighten the generated schema by hand where needed.