Skip to content
ansezz.

▸ Free tool

JSON to TypeScript & Zod.

Paste a JSON sample, get a TypeScript interface tree and a matching Zod v4 schema. It merges array elements into unions, marks the keys that went missing, and detects the string formats worth validating.

▸ One sample is a guess

A single payload cannot show you which keys are optional or which fields vary. Paste an array of real responses — the tool merges them and marks every key that went missing. Nothing is uploaded.

Try

▸ Options

Declaration
// Paste a JSON sample above.

What the generator infers

Every value in the sample collapses into one node that records which shapes were seen at that position, and then the nodes get merged. Array elements merge into a single element type, so [1, "a", true] becomes number | string | boolean rather than a tuple. When every element is an object, the keys are unioned and each one is counted: a key that appeared in two of three elements is marked optional. null is tracked as a modifier instead of a member, so a field that was sometimes a string and sometimes null comes out as string | null and .nullable(), not as a union containing null.

Strings are then probed for shapes worth validating — UUID, email, http(s) URL, YYYY-MM-DD, and ISO-8601 datetimes. Timezone handling is deliberately literal: a trailing Z emits z.iso.datetime(), a numeric offset emits z.iso.datetime({ offset: true }), and no timezone at all emits z.iso.datetime({ local: true }), because the default form rejects the other two. If one field mixes formats, the detector gives up and emits a plain z.string() — dropping to a looser type is never wrong, and a field with three date formats in it is a data problem you want to see, not paper over. Numbers get z.int() only when every sample was a whole number.

One sample under-specifies a schema

This is the thing every JSON-to-TypeScript tool quietly gets wrong, mine included, and there is no fixing it from the input side. A single response is one point on a distribution. It cannot tell you that coupon is absent for 98% of orders, that status is really one of four strings, that items is occasionally empty, or that discount: null is a number the rest of the time. Optionality and unions are properties of the set of possible payloads, and you handed the generator exactly one.

So change the input. Grab twenty real responses from your logs, wrap them in an array, and paste that. The merge does the work: keys that vary get the ?, fields that vary get a union, and you find out in ten seconds that a field you thought was string is string | number in production. Same trick for a paginated endpoint — paste the page of results, not one item.

Parse, don't validate

A TypeScript interface is a claim, not a check. JSON.parse returns any, and const user = await res.json() as User is a lie the compiler believes: it deletes the type error without deleting the bug. Everything downstream is now typed against a shape nobody verified, and the undefined is not an object lands four call frames away from the response that caused it.

Parse, don't validate means turning unknown input into a value whose type proves the checks already ran. That is what the Zod tab is for. const user = UserSchema.parse(await res.json()) either throws at the boundary with a path telling you which field was wrong, or hands back a value the type system and the runtime agree on. The z.infer line at the bottom of the output exists so the type is derived from the schema — write both by hand and they drift within a sprint.

Validate at boundaries, trust types inside

Runtime validation costs something, so spend it where data enters the system and nowhere else: HTTP responses from services you don't own, webhook bodies, queue messages, config files, environment variables, and the JSON an LLM produces for a tool call. Those are the places where the shape can be wrong at 3am. Inside your own module, between two functions you wrote, the compile-time type is enough — re-validating there is ceremony that buys nothing.

One Zod v4 detail worth knowing before you deploy the output: z.object() strips keys it doesn't know about. If you validate a webhook and then forward the parsed value onward, the fields you didn't model are gone. Use z.looseObject() when you re-serialize, and z.strictObject() when an unexpected key should be a loud failure.

Zod v4 syntax, not v3

Most generators on the web still emit Zod 3. The v3 method chains mostly keep working in v4, but they are deprecated, they are not tree-shakable, and one of them is now an outright type error — z.record() requires a key schema as well as a value schema. Here is the mapping this tool emits:

  Zod 3 Zod 4
Email z.string().email() z.email()
URL z.string().url() z.url()
UUID z.string().uuid() z.uuid()
ISO datetime z.string().datetime() z.iso.datetime()
Integer z.number().int() z.int()
Record z.record(z.unknown()) z.record(z.string(), z.unknown())
Unknown keys z.object({ … }).passthrough() z.looseObject({ … })

The Date option is the one place the two tabs deliberately disagree with the wire format. JSON has no date type, so the honest TypeScript type for a timestamp is string. Tick the box and you're saying the type describes the value after validation: the TypeScript side becomes Date, and the Zod side validates the ISO string first and then transforms it, which is stricter than coercing whatever the Date constructor happens to accept.

Where not to trust the output

Read the generated code as a draft written by something that saw one example. In particular:

Questions, answered

Does this JSON to TypeScript converter upload my data?

No. Parsing, inference and code generation all run in your browser — there is no request, no logging and nothing stored. That matters here more than for most tools, because the JSON people paste into a type generator is usually a real API response with real customer records in it.

Why are some fields marked optional in the generated interface?

A key is optional when it appeared in some merged object samples but not all of them. That only happens when you paste an array of objects, or when the same nested shape shows up more than once with different keys. Paste one object and nothing can be optional — the tool has no evidence either way, so every key comes out required.

How do I generate a Zod schema from JSON?

Paste the JSON, then switch the output to the Zod tab. You get one z.object() per inferred type, ordered so every schema is declared before it is referenced, plus an exported z.infer type so the TypeScript side stays derived from the schema instead of duplicated by hand.

Is the output Zod v4 or Zod v3 syntax?

Zod v4. String formats are the top-level functions — z.email(), z.url(), z.uuid(), z.iso.datetime() — integers are z.int(), and records are z.record(z.string(), z.unknown()) with both a key and a value schema. The v3 method chains like z.string().url() still run in v4 but are deprecated, and z.record() with one argument is a v4 type error.

Should I use generated types in production?

Use them as a first draft, not a contract. A generator sees one payload, so it cannot know that status is an enum of four values, that a field is optional, or that an id you got as a number is an int64 that will lose precision. Read every line, tighten the loose ones, and keep the schema in version control from then on.

How does the tool handle null values in JSON?

When a field was sometimes null and sometimes a value, null becomes a modifier rather than a union member: string | null in TypeScript, .nullable() in Zod. When a field was null in every sample, you get the literal null type and z.null(), and the stats line flags it as always null. That is the honest answer — the sample proves nothing else about that key — and the compile error the first time you assign to it is the point.

Related

Keep reading