Hunt
Skip to content

JSON to Zod Schema

Paste a JSON sample and get a Zod schema that validates data of the same shape at runtime, plus the TypeScript types inferred from it. Nested objects become named schemas, and emails, URLs, UUIDs and ISO dates are detected.

Runs in your browser

JSON

Paste JSON or press Sample

Zod schema

The Zod schema will appear here

What this tool does

TypeScript types disappear when your code is compiled, so they can't catch an API that returns something unexpected. A Zod schema checks the data at runtime, and z.infer gives you the TypeScript type from the same source, so the two can't drift apart.

Writing the schema for a large response by hand is tedious. This tool reads a sample and writes a Zod 4 schema for it, in your browser: paste real responses without anything being uploaded.

How to use the JSON to Zod Schema

  1. Paste a JSON sample into the left panel, or press Sample to try an example.
  2. The Zod schema appears on the right as you type.
  3. Set the root name, choose how unknown keys are handled, and turn format detection and exported types on or off.
  4. Copy the result or download it as schema.ts, then install Zod (npm install zod) in your project.

Examples

Generate a schema and its type

Input
{"id":7,"name":"Ada","active":true}
Output
import { z } from "zod";

export const RootSchema = z.object({
id: z.number(),
name: z.string(),
active: z.boolean(),
});
export type Root = z.infer<typeof RootSchema>;

Optional and nullable fields

Input
{"orders":[{"id":"o1","coupon":"SAVE10","note":null},{"id":"o2","note":"gift"}]}
Output
import { z } from "zod";

export const OrderSchema = z.object({
id: z.string(),
coupon: z.string().optional(),
note: z.string().nullable(),
});
export type Order = z.infer<typeof OrderSchema>;

export const RootSchema = z.object({
orders: z.array(OrderSchema),
});
export type Root = z.infer<typeof RootSchema>;

coupon is missing from the second order, so it is optional. note is present in both but null in one, so it is nullable.

Formats are detected

Input
{"id":"123e4567-e89b-12d3-a456-426614174000","email":"ada@example.com","website":"https://jshunt.dev","createdAt":"2026-11-08T09:41:22Z"}
Output
import { z } from "zod";

export const UserSchema = z.object({
id: z.uuid(),
email: z.email(),
website: z.url(),
createdAt: z.iso.datetime({ offset: true }),
});
export type User = z.infer<typeof UserSchema>;

Reject unknown keys, without types

Input
{"name":"Ada","tags":["math"]}
Output
import { z } from "zod";

export const RootSchema = z.strictObject({
name: z.string(),
tags: z.array(z.string()),
});

How the schema is built

The same rules as the JSON to TypeScript generator, written as Zod:

  • Objects become named schemas: a customer object becomes CustomerSchema, and arrays of objects use the singular (orders → OrderSchema). Child schemas are declared before the schemas that use them, as Zod requires.
  • Optional fields: a key missing from some items of an array gets .optional(). A value that is sometimes null gets .nullable().
  • Unions: values of different types become z.union([…]).
  • Formats: with Detect formats on, a string field becomes z.email(), z.url(), z.uuid(), z.iso.datetime({ offset: true }) or z.iso.date() when every value in the sample matches that format.
  • Empty values: an empty array becomes z.array(z.unknown()) and an empty object z.record(z.string(), z.unknown()).
  • Types: each schema gets a matching export type … = z.infer<typeof …>, unless you turn Export types off.

Tighten the schema before you rely on it

A schema generated from one sample accepts that sample, but it can't know your rules. Review it and add what only you know:

  • Fixed sets of values: replace z.string() with z.enum(["draft", "published"]).
  • Number rules: add .int(), .positive() or .min() where they apply.
  • Fields that are always null in the sample get z.null(); widen them, for example to z.string().nullable().
  • Unknown keys: choose whether objects strip, reject or keep keys the schema doesn't list (see Options).

Options

Root name
Names the top-level schema and type: User gives UserSchema and User.
Unknown keys
What object schemas do with keys they don't list. Strip (z.object, the default) removes them from the parsed result; Reject (z.strictObject) fails validation; Keep (z.looseObject) passes them through.
Detect formats
Uses Zod's format schemas for strings when every sample value matches: email, URL (http or https), UUID, ISO date-time and ISO date. Turn it off to get z.string() everywhere.
Export types
Adds export type Name = z.infer<typeof NameSchema> after each schema.

Limitations

  • The schema describes one sample. It can't know enums, number ranges, or fields the sample doesn't include.
  • The output uses Zod 4 syntax (z.email(), z.iso.datetime(), z.strictObject). Zod 3 projects need small changes, such as z.string().email().
  • Strict JSON only. To fix invalid JSON first, use the JSON Fixer.
  • Inputs up to 10 MB are supported. Generation runs in your browser.

Frequently asked questions

Is my JSON sent to a server?

No. The schema is generated entirely in your browser. Nothing you paste is uploaded, stored or logged.

Why use Zod instead of TypeScript types?

TypeScript checks your code when it compiles; it can't check data that arrives at runtime from an API, a file or a form. Zod validates that data, and z.infer derives the TypeScript type from the schema, so you only maintain one definition.

Does it work with Zod 3?

The output targets Zod 4. For Zod 3, replace the format helpers with their older forms (z.string().email(), z.string().url(), z.string().uuid(), z.string().datetime({ offset: true }), z.string().date()), and z.strictObject({…}) with z.object({…}).strict().

What is the difference between optional and nullable?

.optional() means the key may be missing; .nullable() means the key is present but its value may be null. The tool chooses based on what your sample shows, and uses both only if a field is sometimes missing and sometimes null.

How do I validate data with the generated schema?

Call RootSchema.parse(data) to get typed data or an exception, or RootSchema.safeParse(data) to get a result object you can check without try/catch.