JSON to TypeScript
How to Generate TypeScript Types From JSON
Paste a representative JSON sample — an API response, a config file, a fixture — into the left panel. Interfaces appear on the right as you type. Set the root name to whatever the payload represents, and every nested interface is named after it, so a document called SearchResponse produces SearchResponseResult and SearchResponseResultAddress rather than three interfaces called Item.
Everything runs in your browser. The sample is never uploaded, which matters more here than for most tools, because the fastest way to get accurate types is to paste a real production response — and real responses contain customer names, internal identifiers, and email addresses.
What the Generator Infers
Load the example and read the output alongside the input. Five inference rules are doing all the work:
| In the sample | Generated | Rule |
|---|---|---|
"name": "Ada" | name: string | Primitives map directly |
"invitedBy" on one array element only | invitedBy?: string | A key missing from some elements is optional |
"score": 91.5 and "score": null | score: number | null | Conflicting types union, with null last |
"address": { … } | address: RootResultAddress | Nested objects become their own interface |
"tags": ["admin","eu"] and "tags": [] | tags: string[] | An empty array yields to a typed sibling |
That last rule is worth dwelling on. An empty array carries no type information, so the honest inference is unknown[] — but emitting string[] | unknown[] because one record in your sample happened to have no tags is a type nobody wants. When a typed array appears anywhere alongside an empty one, the empty one is discarded. An array that is empty in every sample still infers as unknown[], because at that point there is genuinely nothing to go on.
Why an Array Sample Beats a Single Object
The single highest-value thing you can do is paste a sample containing several records rather than one. Optionality cannot be inferred from one object: every key it has looks required, and every key it lacks does not exist. Given two records where only the second carries invitedBy, the generator can tell you the field is optional — which is precisely the information that prevents an undefined-property crash three weeks later.
The same applies to nullable fields. A field that is null in one record and a number in another produces number | null, and TypeScript will then force you to handle the null at every use site. If your sample happens to contain only populated records, you get number, and the null arrives in production instead. When in doubt, paste a full page of results.
Identical Shapes Become One Interface
Two objects with the same keys and the same types generate a single interface used in both places. Given {"from":{"x":1,"y":2},"to":{"x":3,"y":4}}, the output declares RootFrom once and types both properties with it, rather than emitting a duplicate called RootTo.
This is what keeps output from a paginated or deeply repetitive payload readable — without it, a response containing the same address shape in four places yields four identical interfaces with numeric suffixes. The trade-off is that the surviving name comes from whichever key was encountered first, so you will sometimes want to rename it. That is a one-line edit, and it is a much better default than deduplicating nothing.
The Four Options
export is on by default, since generated types almost always live in a types.ts that something else imports. Turn it off when you are pasting into the file that uses them.
type instead of interface emits type aliases. Interfaces are the better default — they produce shorter error messages, and they support declaration merging, which is how you extend a generated type without editing the generated file. Choose type when your codebase has standardised on it, or when you intend to build unions and mapped types on top.
readonly properties marks every field immutable. This is the right call for API responses, which you should be treating as immutable snapshots, and the wrong one for a form model you intend to mutate.
unknown over any controls what an empty array or an unknowable value becomes. unknown is strictly safer: it accepts any value on the way in but forces a narrowing check before use, whereas any disables type checking entirely and silently propagates through every expression it touches. Prefer unknown unless you are pasting into a codebase that is not ready for the compile errors it will surface.
What Generated Types Cannot Tell You
RootResult[] does not check anything when the response arrives — TypeScript removes the annotation during compilation, and a payload that violates it flows straight through. Types catch mistakes in your code, not bad data from someone else's server. If the boundary needs to be enforced, pair the generated interface with a runtime validator such as Zod or Valibot, and parse at the fetch call.
string and an epoch value is typed number — our Unix timestamp converter is useful for working out which you are looking at. IDs beyond 253−1 are typed number and will already have lost precision during parsing. A status field is typed string where a union of literals such as "active" | "banned" would be far more useful. Each of these is a deliberate narrowing you make after generating.
Where This Fits in a Workflow
Consuming an undocumented internal API. The common case. Capture one response from the network tab, paste, and you have types in seconds instead of hand-writing an interface from a formatted blob. Run the response through our JSON formatter first if it arrived minified.
Typing a config file. Turn tsconfig-style or CI configuration into an interface so a script that reads it is checked at compile time. Config written as YAML can be converted with our YAML to JSON converter first, since the two formats share a data model.
Catching a contract change. Regenerate types against a fresh sample after an upstream deploy and compare them with the ones in your repository. Doing the same check on the raw payloads with our JSON diff tool shows you which field moved, and the regenerated interface shows you whether the change breaks compilation.
Frequently Asked Questions
type Root = RootItem[] — so you have a name for the collection and a name for the element. A bare value such as 42 produces type Root = number, which is valid JSON under RFC 8259 and valid TypeScript.content-type or 2fa becomes "content-type": string and "2fa": boolean, which is legal TypeScript and preserves the key exactly. Renaming it would produce a type that no longer describes the payload.number | string | null[] would mean something different — it parses as a number, or a string, or an array of nulls. Wrapping it as (number | string | null)[] is the only way to express an array whose elements may be any of the three.