▸ 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.
▸ Options
// Paste a JSON sample above.
// 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 | |
|---|---|---|
| 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:
- Enums are invisible.
"status": "paid"gives youstring. If it is really"paid" | "pending" | "refunded", only you know that. - Big integers. JSON numbers are IEEE-754 doubles. A 64-bit
id past 2^53 has already lost precision by the time
JSON.parsereturns, andz.int()only asserts the safe-integer range — it cannot recover what the parse threw away. Ask the API for a string. - Optional and nullable are different bugs. A missing key and
a
nullkey fail in different places. The selector above exists because plenty of APIs do both, inconsistently, on the same field. - Semantics never survive.
z.email()checks a shape, not a mailbox. A number field that must be positive, a string with a max length, two fields that have to agree — all of that is.min(),.max()and.refine()you add by hand. - Empty containers say nothing.
[]becomesunknown[]and{}becomesRecord<string, unknown>, because that is genuinely all the sample proves. Same for a key that wasnullevery time: you get thenulltype and a chip in the stats line saying so. Go find a payload where they aren't empty.
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
- Tool JSON Formatter Pretty-print, minify and sanity-check the payload before you turn it into a type.
- Blog API vs MCP Tool schemas are the same problem in a different costume: a contract a model has to satisfy at runtime.
- Blog REST vs gRPC What you get when the schema comes first and the wire format is generated from it, instead of the other way round.
Keep reading
-
▸ Tool
JSON Formatter & Validator
Fix the sample first: format, validate, and find the exact broken character.
-
▸ Tool
URL Parser & Encoder
The other half of an API payload — query params, and the four encoders people mix up.
-
▸ Post
/blog/rest-vs-grpc/
Schema-first vs sample-first, and what you give up choosing either.
-
▸ Post
/blog/api-vs-mcp/
Typed schemas stop being a nicety once an agent is picking the tool.