Skip to content

JSON to Zod Schema Generator

Convert JSON to Zod schemas with their inferred TypeScript types, ready for parse or safeParse.

Input · JSON
Output · Zod
Valid JSONRuns in your browser

Load from a URL

Your browser fetches it directly, so the server must allow cross-origin requests (CORS).

From signup JSON to Zod schema

With Signup as the Root name, the sample signup form body becomes:

{
  "email": "sam@example.com",
  "password": "correct-horse",
  "displayName": "Sam",
  "age": 29,
  "acceptTerms": true,
  "interests": ["hiking", "jazz"],
  "referral": { "code": "WELCOME10", "source": "newsletter" }
}
import { z } from "zod";

export const ReferralSchema = z.object({
  code: z.string(),
  source: z.string(),
});
export type Referral = z.infer<typeof ReferralSchema>;

export const SignupSchema = z.object({
  email: z.string(),
  password: z.string(),
  displayName: z.string(),
  age: z.number().int(),
  acceptTerms: z.boolean(),
  interests: z.array(z.string()),
  referral: ReferralSchema,
});
export type Signup = z.infer<typeof SignupSchema>;
  • ReferralSchema comes first because a const can’t be used before the line that defines it.
  • age held a whole number, so it got .int(); one decimal value would have made it plain z.number().
  • Each schema is followed by its z.infer type, so the TypeScript type can’t drift from the validator.

Download saves this as signup.ts, the file imported below.

Null and missing values

Array items are merged before anything is written (rules on JSON to TypeScript), and the result turns into method calls. With Plans as the root name:

[
  { "plan": "pro", "seats": 5, "coupon": "SPRING" },
  { "plan": "free", "seats": 1, "coupon": null },
  { "plan": "team", "seats": 12 }
]
import { z } from "zod";

export const PlanSchema = z.object({
  plan: z.string(),
  seats: z.number().int(),
  coupon: z.string().nullable().optional(),
});
export type Plan = z.infer<typeof PlanSchema>;

export const PlansSchema = z.array(PlanSchema);
export type Plans = z.infer<typeof PlansSchema>;

coupon was a string, then null, then missing, so it ends in .nullable().optional() and infers as string | null | undefined. An array root also gets its own schema, PlansSchema.

Validate with parse or safeParse

parse returns typed data or throws a ZodError; safeParse returns { success: true, data } or { success: false, error }:

import { SignupSchema } from "./signup";

export async function POST(req: Request) {
  const result = SignupSchema.safeParse(await req.json());
  if (!result.success) {
    return Response.json({ issues: result.error.issues }, { status: 400 });
  }
  const signup = result.data; // typed as Signup
  return Response.json({ welcome: signup.displayName });
}

Each issue has a path, such as ["referral", "source"], and a message. For per-field form errors, call z.flattenError(result.error) in Zod 4 or result.error.flatten() in Zod 3. Unknown keys are dropped from result.data; .strict() rejects them instead.

Rules a sample can’t show

A sample gives types, not constraints, so email and password are plain z.string(). Add what you know with .extend():

import { z } from "zod";
import { SignupSchema } from "./signup";

export const StrictSignupSchema = SignupSchema.extend({
  email: z.email(),
  password: z.string().min(8),
  age: z.coerce.number().int(),
});

z.email() is Zod 4 (z.string().email() in Zod 3), and z.coerce.number() turns a form’s "29" into 29. Zod 4’s .int() only accepts safe integers, up to 9,007,199,254,740,991, so larger 64-bit IDs sent as numbers fail; JSON.parse loses their digits anyway, so send them as strings.

For a contract other languages can read, use the JSON Schema generator and check data against it with the JSON Schema validator.

Questions

Does the output work with Zod 3 and Zod 4?

Yes. It only uses z.object, z.string, z.number with .int(), z.boolean, z.array, z.union, z.unknown, .nullable() and .optional(), which both versions have, imported from "zod".

How do I make every field optional for a PATCH request?

Call SignupSchema.partial(). It returns a copy of the schema in which every key is optional.

Can it generate z.enum for fields with a fixed set of values?

No. A sample cannot tell a fixed list from free text, so strings come out as z.string(). Replace them with something like z.enum(["free", "pro", "team"]) where you know the values.

Can I use the schema with React Hook Form?

Yes. Import zodResolver from @hookform/resolvers/zod and pass zodResolver(SignupSchema) as the resolver option of useForm.

Related tools

Esc