How to generate typescript types from api response json
- Step 1Capture a full success response — Hit the endpoint in Postman or copy the response from DevTools → Network → Response. Use a request that returns every field populated; a sparse response generates an incomplete type, and the generator won't infer keys that aren't present.
- Step 2Paste and name the envelope — Drop the JSON in and set 'Root type name' to the response type, e.g.
UsersResponseorOrderDetail. Nested objects auto-name from their keys, so{ data, pagination }yieldsData/DataItemandPaginationautomatically. - Step 3Pick output style — Leave 'Use interface' on for
interface UsersResponse, or switch totypeif you'll build discriminated unions (success | error) from the pieces. Leave 'Export all types' on so the whole set can live in one importable module. - Step 4Keep optional off for responses — For API responses you usually want a strict shape — leave 'Optional properties' off. Turn it on only for a deliberately-loose draft; it marks every field
?and won't detect which fields the API actually omits. - Step 5Generate, copy, place — Click 'Generate Types', then Copy or Download .ts. Drop the result into
types/api.tsor co-locate it next to the fetch function. - Step 6Annotate the fetch wrapper — Use the type at the boundary:
async function getUsers(): Promise<UsersResponse> { const r = await fetch('/api/users'); return r.json() as Promise<UsersResponse>; }. Widen anyfield: nullplaceholders toT | null, and split success/error shapes into a union with a type guard.
The four real generator options
These are the only controls in the tool's UI (app/tool/json-to-typescript). There is no indent toggle (output is always 2-space), no per-field optional detection, and no Date / branded-type inference — the generator maps JSON types to TS types one-to-one.
| UI control | What it does | Default | Notes |
|---|---|---|---|
| Root type name (text box) | Names the outermost type. Root by default; type User, ApiResponse, etc. | Root | Nested object types are named from their key (a address key becomes interface Address), not from this box |
Use interface (vs type) | Emits interface Name { ... } when on; type Name = { ... }; when off | On | Both forms are structurally identical; the toggle is purely stylistic |
| Export all types | Prefixes every emitted type with export | On | Turn off for types you'll re-export from a barrel file or keep module-private |
| Optional properties (?) | Adds ? to every field of every type at once | Off | This is a single global switch — it does NOT detect which keys are sometimes-missing. See the edge cases |
How JSON values map to TypeScript
The mapping is driven entirely by the JavaScript runtime type of each parsed value — there is no schema, no format detection, and no heuristics beyond these rules.
| JSON value | Generated TS type | Detail |
|---|---|---|
"text" | string | Every string is string — no string-literal union, no email/uuid/date-string narrowing |
42 or 9.5 | number | Integers and floats both become number. (The sibling json-to-zod does emit .int() for integers — TypeScript has no integer type, so this tool can't) |
true / false | boolean | No literal true/false narrowing |
null | null | A null value produces the literal type null — not string | null. There is no nullable inference |
{ ... } | named interface/type | Named from the key (or the root name). Emitted as a separate top-level type and referenced by name |
[ ] (empty) | unknown[] | An empty array can't be sampled, so the element type is unknown |
[1, 2, 3] | number[] | Element type inferred from the array contents |
[1, "a"] | (number | string)[] | Mixed-type arrays produce a union element type |
[{...}, {...}] | <Name>Item[] | Object arrays produce one item interface — sampled from the first element only |
Free vs Pro generation limits
JSON to TypeScript is a Pro tool gated by the shared codeGeneration quota (lib/preview-quotas.ts). Limits are daily-run counts plus a row cap that applies when the JSON root is an array.
| Tier | Runs/day | Array row cap | File size |
|---|---|---|---|
| Free | 1 | 100 rows | 2 MB |
| Pro | 5 | 1,000 rows | 100 MB |
| Pro Media | 50 | 10,000 rows | 100 MB |
| Developer / Enterprise | Unlimited | Unlimited | 100 MB+ |
Cookbook
Real endpoint responses in, the verbatim TypeScript the generator emits out, and the refinements an API client author makes next.
Paginated list response
ExampleThe canonical { data: [...], pagination: {...} } envelope. The generator emits the wrapper, a DataItem, and a Pagination interface.
Input:
{ "data": [ { "id": 1, "name": "Ada" } ], "pagination": { "page": 1, "total": 42 } }
Output (rootName: UsersResponse):
export interface UsersResponse {
data: DataItem[];
pagination: Pagination;
}
export interface DataItem {
id: number;
name: string;
}
export interface Pagination {
page: number;
total: number;
}Bare array response
ExampleWhen the endpoint returns a top-level array, set a descriptive root name; the generator emits a single item interface named <Root>Item.
Input (GET /tags):
[ { "id": "a1", "label": "news" }, { "id": "b2", "label": "sport" } ]
Output (rootName: Tag):
export interface TagItem {
id: string;
label: string;
}
// The array type is TagItem[]; annotate as Promise<TagItem[]> in your client.Nullable field needs widening
ExampleAn endpoint returning null for deletedAt produces the literal null — widen it to the real nullable type by hand.
Input:
{ "id": 7, "deletedAt": null, "createdAt": "2026-06-10T12:00:00Z" }
Output (verbatim):
export interface Root {
id: number;
deletedAt: null;
createdAt: string;
}
Hand-edit:
export interface Record {
id: number;
deletedAt: string | null; // ISO timestamp or null
createdAt: string;
}Success and error shapes as a union
ExampleThe generator types one response at a time. Generate the success and error responses separately, then combine them into a discriminated union with a type guard.
Generate from a 200 body → SuccessResponse
Generate from a 4xx body → ErrorResponse
Combine by hand:
type ApiResult = SuccessResponse | ErrorResponse;
function isError(r: ApiResult): r is ErrorResponse {
return "error" in r;
}Array sampled from the first element
ExampleIf the first list item is missing a field that later items have, the field won't appear. Reorder so the richest record is first, or paste a single merged superset object.
Input:
{ "data": [ { "id": 1 }, { "id": 2, "premium": true } ] }
Output:
export interface Root {
data: DataItem[];
}
export interface DataItem {
id: number;
}
// 'premium' from the second element is NOT in DataItem. Reorder the sample.Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Array payload sampled from first element
By designObject arrays infer the item interface from the first element only — keys exclusive to later elements are absent. Reorder so the most complete record is first, or paste a single merged superset object before generating.
Success/error envelope not auto-discriminated
By designThe generator types one body at a time and never produces a discriminated union. Generate the 200 and the 4xx bodies separately, then union them and write a r is ErrorResponse type guard by hand.
Nullable field typed as `null`
ExpectedA null value yields the literal null type, not T | null, because the sample carries no other type. Widen every field: null by hand, or capture a response where the field is populated to infer the underlying type.
No OpenAPI / schema inference
Not supportedThis tool samples a concrete JSON value; it does not read OpenAPI/JSON Schema. For spec-driven generation on APIs you own, use openapi-typescript as a build step. For one-off shapes from real responses, this tool is faster.
Numeric IDs over 2^53 lose precision before generation
Upstream precision lossJSON.parse represents numbers as IEEE-754 doubles, so a 19-digit snowflake ID is already imprecise before the generator sees it — and it's typed number regardless. If the API sends big integers as JSON numbers, change them to strings server-side; the type should then be string.
Map-like object becomes a fixed-key interface
ExpectedA { "en": "...", "fr": "..." } map is emitted as an interface with literal keys en/fr. If the keys are dynamic, replace it with an index signature { [code: string]: string } or Record<string, string> by hand.
Empty array field
unknown[]An empty errors: [] becomes errors: unknown[] — there's nothing to sample. Replace with the real element type, e.g. ApiError[], or capture a response where the array is populated.
Date/time strings typed as string
ExpectedISO timestamps and date strings become string, which is correct for the serialized response. Parse to Date in your client; the wire type is string.
Duplicate nested type names collide
First write winsIf two different nested objects share a key (e.g. author and editor both contain profile of different shapes), only the first Profile interface is emitted and both reference it. Rename one key, or generate the branches separately.
Invalid JSON pasted
Parse errorThe input is run through JSON.parse after trimming; trailing commas, comments, or single quotes throw and the error shows in the UI. Repair with json-format-fixer first, then regenerate.
Frequently asked questions
How do I type an endpoint that returns either a success or an error shape?
Generate the two bodies separately — paste a 200 response to get SuccessResponse, paste a 4xx body to get ErrorResponse — then combine them: type ApiResult = SuccessResponse | ErrorResponse;. Add a type guard such as function isError(r: ApiResult): r is ErrorResponse { return 'error' in r; }. The generator handles one concrete value per run and intentionally doesn't try to guess your envelope discriminator.
My JSON is an array of objects with varying keys — why are some fields missing from the type?
When the root (or any nested value) is an array of objects, the generator infers the item interface from the first element only. Keys that appear in later elements but not the first are absent from the generated type; keys in the first element but not later ones are present and non-optional. Reorder your sample so the most complete object is first, or merge a representative superset object by hand before pasting. This is the single most common surprise — the tool samples, it does not union across array elements.
Does it read my OpenAPI / Swagger spec?
No. This tool samples a concrete JSON value and infers types from the runtime shape — it never reads OpenAPI or JSON Schema. For APIs you own, generate from the spec with a build step like npx openapi-typescript api/openapi.yaml -o types/api.d.ts to stay in sync automatically. Use this tool for fast, one-off types from a real response, especially for third-party APIs with no published types.
A field came out as `: null` — that's not a useful type. Why?
JSON null maps to the literal TypeScript type null, because that's the only information present in the sample — the generator has no schema telling it the field is string | null. Wherever you see field: null, the real intent is almost always field: string | null (or another type) and you should widen it by hand. Capture a sample where the field has a non-null value if you want the underlying type inferred instead.
Why is a big numeric ID typed as `number`, and is that safe?
Every JSON number maps to number, and JSON.parse already stored it as an IEEE-754 double — so IDs beyond 2^53 (Twitter/Discord snowflakes, some database bigints) lost precision before the generator even ran. The fix is server-side: emit large integers as JSON strings. The generated type then becomes string, which both preserves the value and types it correctly.
An i18n / dictionary object came out with fixed keys — how do I make it dynamic?
The generator emits an interface with the literal keys present in your sample, e.g. { en: string; fr: string }. When the keys are open-ended (locale codes, user IDs), replace that block with an index signature { [code: string]: string } or Record<string, string> by hand. The tool can't tell a dictionary from a fixed-shape object — both look like the same JSON.
Two different nested objects share a key name — why did one shape get lost?
Nested object types are named from their key, and the generator registers each name only once (first write wins). If a meta object appears in two places with different shapes, both references point to the single Meta interface built from whichever was walked first; the second shape is silently discarded. Rename one of the JSON keys before generating, or generate the two branches separately and rename the resulting interfaces. The collision is structural, not a bug you can toggle off.
Should I keep the types next to the fetch function or in a shared module?
For a small client, co-locating the generated interface with its fetch wrapper is fine. For a larger surface, paste each response into a single types/api.ts with 'Export all types' on so everything's importable from one place. Either way, annotate at the boundary — Promise<UsersResponse> on the fetch function — so consumers get inference for free.
Can I stop the tool from exporting every type?
Yes — uncheck 'Export all types' and the export prefix is dropped from every emitted type. That's useful when you want a single barrel file to control your public surface, or when the generated types are module-private helpers. The toggle is all-or-nothing; there's no per-type export control, so for mixed visibility, generate with export off and add export to the specific types you want public.
How do I get the output into my project?
Use the Copy button to put the generated source on your clipboard, or Download .ts to save a file named after your input with a .types.ts extension (e.g. users.json becomes users.types.ts). The output is plain TypeScript source with one blank line between types — paste it straight into a .ts/.tsx file or a dedicated types.ts module.
What are the size and run limits?
JSON to TypeScript is a Pro tool gated by the shared codeGeneration quota. Free is 1 run per day; if the JSON root is an array, only the first 100 rows are sampled before generation. Pro is 5 runs/day with a 1,000-row sample cap; Pro Media is 50/day at 10,000 rows; Developer and Enterprise are unlimited. The file-size ceiling is 2 MB on free and 100 MB on paid tiers. The row cap only matters for array roots — a single deep object generates the same types regardless of tier because the shape, not the row count, drives the output.
Is my API response data uploaded to a server?
No. Generation runs entirely in your browser via the converter in lib/json-to-typescript.ts — the JSON is parsed and walked client-side, and only an anonymous run counter is recorded when you're signed in. API response data never leaves the tab, which is why you can safely paste real responses that contain customer records, tokens, or internal identifiers.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.