[JSONZen]
Sign up and get Pro free for 3 months — no card required.Claim it →

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

Related tools