JSON to TypeScript
Paste sample JSON, get idiomatic TypeScript interfaces — multi-sample optionality and real unions, entirely on this device.
Enums default off: repeated string values are often coincidence, and the narrowed union would reject new values.
Dates default off: JSON has no date type — date-shaped values stay strings unless you opt in.
export interface Root {
id: number;
tags: string[];
meta: Meta;
}
export interface Meta {
active: boolean;
}
npx quicktype --lang typescript --top-level Root sample.jsonPress ? for shortcuts · Stored only on this device —
Do it offline:$ npx quicktype --lang typescript --top-level Root sample.json
How to use it
- Paste a JSON payload, or drop a .json file on the input pane — the TypeScript regenerates as you edit.
- Two or three payloads beat one: open a sample tab per response, and a key absent from any of them comes back optional.
- Pick interface or type output, then set naming, readonly, enums, dates and the top-level type name.
- Copy the source, save it as a .ts file, or share a link that reopens exactly these samples and options.
Paste sample JSON and get TypeScript ready to import: an interface per object shape, arrays typed from their elements, and mixed values as real unions. Feed it two payloads from the same endpoint and any field missing from one turns optional. Prefer type aliases, readonly properties or preserved key spellings? Flip an option and the pane regenerates — in this tab, with nothing sent anywhere.
How it works
Each sample is first checked by the same strict JSON parser behind this site's JSON validator: a failing sample is reported with line, column and a caret frame, and nothing generates until every tab parses. Valid samples go to quicktype — the Apache-licensed generator embedded in this page — inside a worker thread, so even a multi-megabyte paste cannot freeze the input.
quicktype walks the parsed values and builds one named type per distinct object shape: objects become interfaces, arrays take the type of their elements, and a value seen as more than one kind — a number here, a string there — types as the union of exactly those kinds. An array with no elements types as unknown[] rather than any[]: there is no evidence of an element type, and unknown makes the compiler demand a check before use. Nothing is guessed from key names — a created_at field holding an ISO string stays string until you turn the dates option on.
With several samples loaded, their shapes merge before naming. A key found in every occurrence keeps a plain type; a key missing from at least one gains the ? marker, which is how email?: string records an endpoint that sometimes omits the field. Switching optionality inference off strips those markers from the rendered declarations, so the types assert every key is always present — the reading you asked for, stated plainly.
Naming has two honest modes. Normalize asks quicktype for idiomatic camelCase properties, then annotates each renamed one with an // original comment carrying the wire spelling. Preserve skips renaming and quotes any key that is not a legal identifier. Either way the output pane holds plain declarations only — no runtime converters, no imports — ready to sit next to your fetch call.
Examples
{"id": 1, "tags": ["a"], "meta": {"active": true}}export interface Root {
id: number;
tags: string[];
meta: Meta;
}
export interface Meta {
active: boolean;
}
The default view: the nested object splits into its own Meta interface referenced from Root, the array types from its element, and the column alignment is quicktype's own rendering.
{"id": 1, "email": "a@b.c"}
{"id": 2}export interface Root {
id: number;
email?: string;
}
Two samples, and email appears in only the first — so the merged interface marks it optional while id, present in both, stays required. This is evidence, not guessing.
[1, "a", null]export type Root = Array<number | null | string>;
A heterogeneous array becomes a union of the three kinds actually observed — never a silent any[]. Because the top level is not an object, the name arrives as an exported alias.
{"content-type": "a", "1st": true, "user_id": 3}export interface Root {
"content-type": string;
"1st": boolean;
user_id: number;
}
Preserve mode quotes the two keys that cannot be identifiers and leaves user_id as-is. Under the default normalize mode the same payload renders contentType, the1St and userID instead, each tagged with an // original comment naming its wire key.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
An empty object becomes Record<string, never>
interface Root {} is structurally compatible with almost any value in TypeScript, which would hide bugs instead of catching them. A keyless object is therefore rewritten to Record<string, never> — the type of an object with no known properties — and a chip explains the substitution.
A field that is always null types as null
Inventing string | null from a key's name would be fiction, so a field holding null in every sample keeps the type null and a chip asks for one more payload where it carries a value. One extra sample widens the union to what the API really returns.
Identifier-hostile keys survive, quoted
Keys such as "content-type" or "1st" cannot stand as bare TypeScript property names. Preserve mode quotes them verbatim; normalize mode renames them and files the wire spelling in an // original comment, so a mapping layer can always be written later without re-fetching the payload.
user_id and userId collide after camelCasing
Both spellings normalize toward userID, so quicktype keeps the interface compilable by prefixing one with its owning type — rootUserID beside userID — while a chip lists the colliding originals and the comments say which property carries which key.
Five thousand keys still generate
A sample with 5,000 or more distinct keys is processed in full — nothing is trimmed — but a chip recommends cutting a smaller representative record first, because an interface that long is unreadable in review and slow to type-check.
Recursive shapes reuse their own name
A tree node whose children array holds more nodes renders as children: Root[] — the repeated shape is recognized and referenced by name rather than expanded level by level, so self-referential structures come out as ordinary recursive interfaces.
Scalar and array documents get a top-level alias
[1, 2, 3] renders as export type Root = number[]; and a bare string or number document takes the same alias form. The expression is lifted from quicktype's own converter signature, so whatever the document's top level is, the name you chose is exported and importable.
Similar objects controls cross-path merging, not per-array unions
The Similar objects option drives quicktype's class combining. Combine, the default, folds look-alike object shapes found at different paths into one shared interface and marks keys absent somewhere with the ? modifier; Keep separate leaves each path its own type. Elements within a single array are unified before this choice applies, so an array of varied objects produces one merged element interface under either setting.
Common errors
Input is larger than the 10 MB limit for this tool. For bigger payloads use the CLI: npx quicktype --lang typescript --top-level Root sample.json- Past ten megabytes across all tabs the paste is declined whole — truncating silently would type a payload you never gave it. Inference learns from shapes, and a handful of records carries the same shape information as the full dump; keep the excerpt small or point the printed command at the complete file.
Sample 2: Unexpected '}' at line 1, column 10 — probable cause: trailing comma after the last property.- Every tab must hold strict JSON on its own. The diagnostic names the failing sample, and the caret frame under it points at the offending character — fix that tab or remove it, and the untouched tabs generate again immediately.
Sample 1 is 2.5 MB — generate from a representative sample, not the whole export.- An advisory, not a refusal: generation still ran. Beyond two megabytes a sample mostly adds parse time, because repeated records restate shapes the inference has already seen — a short excerpt produces the same interfaces faster.
Null in every sample: Root.avatarURL — add a sample where it has a value so a real type can be inferred.- The avatar_url field was null everywhere the tool looked, so its property honestly types as null. Add one response where the field is populated and the property widens to the union the endpoint actually produces.
Keys "user_id", "userId" normalize to the same property name — quicktype renamed them to keep properties distinct (see the // original comments).- camelCasing both spellings would mint twin properties, which no interface can hold. One key receives a type-name prefix so the code compiles, and the // original comments record exactly which property answers to which wire key.
FAQ
Should I generate interfaces or type aliases?
Interfaces are the default: most lint presets expect them for plain object shapes, and their compiler errors name the interface rather than restating the whole structure. The type setting emits aliases with member-for-member identical bodies, which suits codebases standardized on type or models that mix into unions. The inferred structure never changes with the style — only the declaration keyword does.
How does the tool decide a field is optional?
By absence, never by name: when samples — or two similar objects merged inside one payload — are combined, a key missing from any occurrence gets the ? marker and keys present everywhere stay required. Turning the inference off removes the markers from the rendered code, so the declarations claim every key always arrives; choose that when you know the gaps were sampling accidents.
Why is enum inference off by default?
Repetition is weak evidence of a closed set: "red" and "blue" recurring in one export might be the only colors, or merely the ones you fetched. Off, such fields stay string and accept tomorrow's values. On, repeated literals become unions such as type C = "red" | "blue", and a chip lists every enum created so no promotion happens behind your back.
Why do timestamps come out as string instead of Date?
Because that is what your program will hold: JSON has no date type, and JSON.parse delivers an ISO 8601 timestamp as a plain string. Typing it Date would promise methods the value lacks at runtime. The dates option offers two alternatives — iso-comment keeps string and appends an // ISO 8601 date-time marker, while date emits Date for code that revives timestamps during parsing.
Does my JSON leave this browser?
No. Parsing and inference run in a worker thread inside this tab, dropped files are read locally, and there is no endpoint to receive samples. Watch the network panel while generating: the only fetch is the tool's own script chunk the first time, never your data.
Can I use more quicktype options than the ones shown here?
Yes — the command printed under the output pane hands your current choices to the quicktype CLI, where the full flag set lives. Treat it as a starting point rather than a byte-exact twin: a few of this page's behaviors, like // original comments and the optionality switch, have no CLI spelling, and the pinned line leaves quicktype's converter scaffolding at its own defaults.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Rerun generation immediately, skipping the debounce delay.
- ⌘/Ctrl + S
- Download the generated TypeScript as a .ts file in place of the browser save dialog.
- ⌘/Ctrl + C
- Copy the whole output when nothing is selected; a live selection copies itself.
- ⌘/Ctrl + Shift + C
- Copy the full generated source even while text is highlighted somewhere.
- t
- Toggle the color theme (inactive inside text fields and IME composition).
- ? or ⌘/Ctrl + /
- Open the shortcut overlay, which shows the glyphs for your OS.
- Esc
- Dismiss the topmost dialog without touching the samples.