Skip to content

JSON to Zod Schema

Generate a Zod schema from a JSON sample, with string formats and integers inferred only when every sample agrees. Zod 4 and 3 output.

JSON sample

paste an array to merge every record

Zod schema

Zod 4
import { z } from "zod";

export const AddressSchema = z.object({
  city: z.string(),
  postcode: z.string(),
});

export const RootSchema = z.object({
  id: z.uuid(),
  email: z.email(),
  age: z.number().int(),
  website: z.url(),
  createdAt: z.iso.datetime(),
  tags: z.array(z.string()),
  address: AddressSchema,
  deletedAt: z.null(),
});

export type Root = z.infer<typeof RootSchema>;

Inference runs in your browser — the sample is never uploaded. A format such as z.email() is only asserted when every string at that position matched it.

About the JSON to Zod Schema

Paste a JSON sample and get a Zod schema back. This JSON to Zod converter infers the shape from the data, names nested objects, marks fields optional when your samples disagree, and asserts string formats such as z.email() and z.uuid() — but only when every sample supports them. Everything runs in your browser.

Paste an array and each record is merged rather than the first one being copied. Real API responses are not uniform: a field is a string in one record and null in another, or missing entirely from a third. A generator that reads only the first element produces a schema that rejects valid data in production. Here, a field absent from some records becomes .optional(), a field that was sometimes null becomes .nullable(), and one that was both becomes .nullish().

The same rule governs every claim made from the values. One string in a hundred that is not an email address, and the field is a plain z.string(). One fractional number, and .int() disappears. A schema that is confidently wrong about a format is worse than one that is merely vague, because it fails only in production and only for the records you did not sample.

Output targets Zod 4 by default and Zod 3 on request. That switch matters more than it looks — see the FAQ.

  • Zod 4 and Zod 3 output, because the string format API changed between them
  • Merges every record in an array, so optional and union types reflect all your samples
  • String formats inferred for UUID, email, URL, ISO datetime, ISO date, time and IPv4
  • Integer detection, emitting .int() only when no fractional value was seen
  • Nested objects extracted into their own named schemas, declared before use
  • Identical shapes deduplicated into a single schema rather than repeated
  • Optional z.infer type export, and z.strictObject when you want unknown keys rejected

How to use it

  1. Paste a JSON object or an array of objects into the left pane.
  2. Pick your Zod version. If your package.json says zod ^3, choose 3 — the generated code will not compile otherwise.
  3. Set the root name. It names both the schema const and the inferred type.
  4. Check the inferred formats against reality. If a field only happens to look like a UUID in your sample, turn format inference off.
  5. Copy the schema. Nested schemas are declared above the ones that use them, so the file compiles as-is.

Real-world use cases

Backend & API developers

Turn a real response sample into a runtime-validated Zod schema for a fetch wrapper or a service boundary, so a malformed payload throws where you can see it instead of surfacing three functions later.

Full-stack TypeScript developers

Get the schema and its z.infer type from the same sample in one step, so the runtime check and the compile-time type cannot quietly drift apart the way a hand-maintained pair does.

Frontend developers consuming third-party APIs

Validate an external service's response shape at the boundary, so a missing or retyped field fails loudly at the fetch call instead of crashing a component deep in the tree.

QA & test engineers

Turn a payload captured from a bug report into a schema that states exactly which fields and types were expected, as a precise artifact to attach to the ticket.

Platform & SDK engineers

Bootstrap request or response validation for a new endpoint from one real payload, then hand-tighten the constraints Zod cannot infer, like value ranges or string length limits.

Examples

Formats inferred from values

Input
{
  "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "email": "[email protected]",
  "age": 36
}
Output
export const UserSchema = z.object({
  id: z.uuid(),
  email: z.email(),
  age: z.number().int(),
});

TypeScript can only say `string` here. This is the whole reason to generate Zod rather than an interface.

An array with inconsistent records

Input
[
  { "id": 1, "nickname": "ada" },
  { "id": 2, "nickname": null },
  { "id": 3 }
]
Output
export const RootSchema = z.array(z.object({
  id: z.number().int(),
  nickname: z.string().nullish(),
}));

Absent in one record and null in another, so .nullish() — the only spelling that accepts both.

One string that is not an email

Input
[
  { "contact": "[email protected]" },
  { "contact": "call the office" }
]
Output
contact: z.string(),

The format claim is dropped entirely rather than applied to most of the data. Formats hold only if every sample agrees.

Zod 3 output for the same input

Input
{ "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8" }
Output
id: z.string().uuid(),

Zod 4 moved these to the top level. Generating the wrong one is the most common reason a copied schema does not compile.

Common errors

MessageCauseFix
z.email is not a functionThe schema was generated for Zod 4 but the project has Zod 3 installed.Switch the version selector to 3, or upgrade Zod. In 3 the spelling is z.string().email().
Expected 2 arguments, but got 1 on z.recordZod 4 requires the key type: z.record(z.string(), z.unknown()). Zod 3 defaulted it.Set the version selector to match your installed Zod. The generator emits the right form for each.
Invalid input: expected string, received nullThe sample never contained a null for that field, so the schema does not allow one.Add a record containing the null to your sample and regenerate, or add .nullable() by hand.
Unrecognized key(s) in objectReject unknown keys was on, so z.strictObject was emitted and the data has a field your sample did not.Turn the option off for API responses you do not control. Strict is right for your own input validation, not for parsing someone else's payload.
Invalid uuid / Invalid email on data that works fineA format was inferred from a sample that happened to match, on a field that is not really constrained to it.Turn off format inference, or delete that one line. A single lucky sample is not evidence of a constraint.
Type instantiation is excessively deep and possibly infiniteA very deeply nested sample produced a deeply nested inline type.Split the schema at a natural boundary and annotate the intermediate types explicitly. The generator already extracts nested objects into their own consts, which usually avoids this.

Frequently asked questions

Is my JSON uploaded?

No. Parsing and generation both run in your browser. Nothing is sent to a server, so you can paste a production API response without it being logged anywhere — and confirm that by using the page with your network disconnected.

Why does the Zod version matter so much?

Zod 4 moved the string formats to the top level: z.email() rather than z.string().email(). The old form still works but is deprecated, and z.record now requires an explicit key type. A schema generated for the wrong major version does not compile, and that is the single most common problem with copied Zod schemas — so it is a switch here rather than a guess.

Should I generate a schema or write one?

Generate first, then edit. Inference can tell you the shape of the data you have; it cannot tell you the constraints your API guarantees — that a string is at most 40 characters, or that a number is positive. Treat the output as an accurate starting point, not a specification.

How many samples should I paste?

As many as you can. Optional fields, nullable fields and union types are all detected by disagreement between records, so one record produces a schema that says every field is always present and never null. An array of twenty real responses gives a far more honest schema than one hand-picked example.

Why is an empty array typed as z.array(z.unknown())?

Because an empty array is genuinely no evidence about its element type. Guessing z.array(z.string()) from an empty array would be inventing a constraint, which is exactly the failure mode this generator is built to avoid. Add a sample containing an element and regenerate.

What is the difference between this and JSON to TypeScript?

A TypeScript interface is erased at build time and checks nothing at runtime. A Zod schema is a value that validates data when it arrives, and it can express constraints the type system has no way to state — that a string is a UUID, or a number an integer. If you want both, keep the schema and derive the type from it with z.infer, which the Export z.infer type option emits for you.

Does it handle nested objects and arrays of objects?

Yes. Nested objects become their own named schema consts, declared before whatever uses them, and arrays of objects have their element schema named in the singular — `users` yields a `UserSchema`. Two positions with an identical shape share one schema rather than producing two copies.

Does a field that is always one of a few values become z.enum()?

No. Inference tracks which *types* were seen at a field, not which literal values — so a `status` field that is always "active" or "archived" in your sample still comes out as a plain z.string(). Recognising a closed set of literals reliably would need far more samples than most people paste, and a wrong enum is worse than none: it rejects the next legitimate value that shows up. Add `.enum([...])` by hand once you are confident the set is exhaustive.

Last updated