JSON to TypeScript Converter

Paste an API response and get the types back. Every entry in a list is merged before a type is written, so a key missing from one record comes through optional instead of silently required.

JSON to TypeScript conversion console

Output
Options
Result
0Types
0Fields
0Depth
0Lines
JSON in
TypeScript out
Waiting for JSON
  • Merges every array entry
  • Quotes illegal keys
  • Runs in your tab

Turning an API response into TypeScript types

Typing a payload by hand is slow, and the errors are the quiet kind. You transcribe fourteen fields correctly, mark the fifteenth as a string when the API sends a number, and nothing complains until a customer hits the path where the value is compared. The generator above reads the sample instead of your memory. What follows is what the four output styles buy you, what a JSON sample never tells you, and where the inference is guessing.

Four ways to land the same payload in TypeScript

Pick by what has to happen after the response arrives. Code drawing a chart from data you already trust wants nothing more than field names. A form posting to a third party API wants the response checked before a single field is read.

OutputWhat you getRuntime costReach for it when
InterfaceNamed shapes, extendable, merged across declarationsNone, erased at build timeThe default. Public shapes a library or another team extends
Type aliasThe same fields, with unions and intersections availableNone, erased at build timeThe shape needs a union, a mapped type, or a name nobody should reopen later
ClassReal objects, a constructor, nested instances built for youShips in the bundleThe payload needs methods, defaults, or nested objects rebuilt after a fetch
Zod schemaParsing, an error naming the field, plus the inferred typeA dependency and a parse call per responseThe JSON comes from an API you do not control

The split worth internalising sits between the first three and Zod. An interface is a note for the compiler and nothing else. Write version: number, let the server send "4", and your build stays green while version + 1 produces the string "41" in production. Zod reads the same shape while the program runs, rejects the string, and names the field. Everything above the Zod row is documentation with autocomplete attached.

Interface against type alias

Both describe an object. The differences are narrow, and two of them decide most cases.

Start with interfaces. Switch a shape to an alias when the shape stops being an object.

Your types are a promise nobody checks

This is the part missed most often, so here it is in code. Both snippets compile. One of them is a lie.

Compiles, no check
const res = await fetch("https://api.toolexe.com/workspace"
);const data: ApiWorkspace = await res.json();data.quota.daily.toFixed(0);
Compiles, checked
const res = await fetch("https://api.toolexe.com/workspace"
);const data = ApiWorkspaceSchema.parse(await res.json());data.quota.daily.toFixed(0);

res.json() is typed any in the standard library, and any assigns to anything. The annotation on the left tells the editor what to autocomplete and produces no code at all after compilation. When the API renames a field or starts sending null, the first sign is a TypeError three call frames away from the fetch.

Two habits keep the gap small. Type the fetch boundary as unknown so an assignment forces a decision, and reach for the Zod output on anything crossing a network you do not own. The unknown rather than any option in the rail follows the same principle for fields the sample leaves undefined, since unknown makes the compiler ask before the value is used.

What a JSON sample never tells you

Inference starts from values, and JSON carries eight types on a good day. These are the places where the generated type is a reasonable reading of the sample rather than the truth about the API.

In the sampleGeneratedWhat the API might mean
"2026-08-14T09:21:05Z"stringA timestamp. JSON has no date type, so parsing stays your job
"GET"stringOften one of five verbs, which wants "GET" | "POST" and a second sample to prove
4 and 1200.5numberTypeScript has one number type, so an integer field and a decimal field look identical
nullnullA field with a value the sample happened to miss
[]unknown[]An array of records nobody created yet
["a", 1](string | number)[]A fixed pair, which wants the tuple [string, number]
9007199254740993numberAn identifier past the safe integer range, which belongs in a string
{"eu-west": {...}}Two oddly named fieldsA lookup, which wants Record<string, Region>

The identifier row bites hardest and shows up in production data. Numbers in JSON parse into IEEE 754 doubles, so an ID above 9007199254740991 comes back changed. Snowflake IDs from Discord and X, Twitter style cursors and some database sequences all live past the line. The API sending them as strings is doing you a favour, and the generated string is correct rather than lazy.

Paste the whole list, not one entry

Every item in an array is folded into a single shape before a type is written. A key present in all entries stays required. A key present in some of them comes through optional, and the decision log names the count. This is the reason to paste a full response rather than a trimmed record.

JSON in
"endpoints": [{ "path": "/json/beautifier","method": "GET","cache-ttl": 300 },{ "path": "/json/json-to-typescript","method": "GET" }]
Interface out
export interface Endpoint {path: string;method: string;"cache-ttl"?: number;}

Three decisions are visible there. The plural key endpoints was made singular for the type name, so the field reads Endpoint[] rather than Endpoints[]. The second entry has no cache-ttl, so the field is optional and the log says one of two records was missing it. And cache-ttl is quoted, because a dash is legal in a JSON key and illegal in a TypeScript identifier.

Keys JSON allows and TypeScript will not

A JSON key is any string. An identifier is not, and payloads break the rule constantly, mostly because kebab-case reads well in a config file nobody writes code against.

Renaming a key rather than quoting one would break the payload, so the generator never does. A camelCase codebase reading a snake_case API needs a mapping layer, and Zod gives you the natural place for one with .transform() after the parse.

What the class output builds for you

The three type styles vanish at build time. The class style produces code, and the constructor is the reason to pick it. Unpacking a response into a class you wrote by hand fills the top level and stops, so a nested field stays the plain object JSON.parse produced.

Object spread, shallow
const w = Object.assign(new ApiWorkspace(), raw);w.owner instanceof Owner;
Generated constructor
const w = new ApiWorkspace(raw);w.owner instanceof Owner;w.endpoints[0] instanceof Endpoint;

Each nested object gets its own class, the constructor calls new Owner(data.owner), and arrays of objects are mapped one entry at a time. Methods you add to Owner work on data straight off the wire. The cost is real: classes are values, so they ship in the bundle, and a shape used only for typing pays that weight for nothing.

Reading the decision log

The panel under the editors lists what the generator worked out and what the sample left open. Read it before pasting anything into a repository. Green markers are decisions made from solid evidence, such as a quoted key or two blocks sharing one type. Amber markers are gaps in your sample: an empty array, a null value, a field missing from some entries, a key holding more than one shape. Every amber line is a spot where a second response would produce a better type.

Where this converter stops

Nothing you paste leaves the page. Parsing, inference and code generation all run in your browser, so a response carrying a customer record or a bearer token stays on your machine. Load the page once, drop your connection, and everything above still works.

Questions about generating TypeScript from JSON

Interfaces against type aliases, optional fields, quoted keys, and the payloads inference reads wrong.

Should I generate an interface or a type alias?

Start with an interface. Both describe the same object and both disappear at build time, so the choice comes down to two behaviours. Interfaces sharing a name merge into one declaration, which lets a consumer extend a shape your library exports. Type aliases refuse to merge, which is the safer default inside an application where a silent extra field would be a surprise. The real dividing line is unions. Status holding one of four strings, or an ID typed as string or number, has to be an alias, since an interface only describes objects. Compiler errors also read better against interfaces, because the name survives instead of the whole shape being expanded.

Do these types check my API response at runtime?

No, and the interface, type alias and class styles never will. Annotations are erased when TypeScript compiles, so the generated code has no idea what the server sent. Worse, res.json() is typed any in the standard library, and any assigns to any annotation without complaint. A renamed field or a number arriving as a string sails through the build and fails later in unrelated code. The Zod output is the answer to exactly this. ApiWorkspaceSchema.parse(await res.json()) throws on a bad payload, names the field and the block, and hands back a value the inferred type describes honestly.

Why is one of my fields optional when the value is right there?

That comes from a list. Every entry in an array is folded into one shape, so a key present in three of five records becomes optional, and the decision log names the count. It is a feature rather than a slip, since a required field missing from real data is the bug people ship. If the field should always be present, your sample is saying it is not, and the response is worth a second look before the type is. The same rule fires when the null option is on, since a field holding null in the sample has no known filled type.

Why did my key come out in quotes?

Because a dash, a dot, a space or a leading digit is legal in a JSON key and illegal in a TypeScript identifier. The output quotes cache-ttl and the type stays correct, though access moves to data["cache-ttl"] since dot notation stops working. Renaming the key would break the payload, so the generator leaves it alone. A camelCase codebase reading a snake_case API wants a mapping layer, and the Zod output gives you the place for one with .transform() after the parse.

My timestamps came out as string. Can I get Date?

Not from the sample alone, because JSON has no date type. A timestamp is a string in the payload, so string is the honest reading, and JSON.parse hands your code a string too. Changing the annotation to Date without changing the parse would make the type lie. Two ways forward. Keep the string and call new Date(value) where you need the object, or switch to the Zod output and add z.coerce.date() on the field, which converts and validates in the same pass.

What does the class output give me over an interface?

Nested objects rebuilt after a fetch, plus somewhere to hang methods. JSON.parse returns plain objects all the way down, and Object.assign into a class instance fills the top level only, so a nested field never becomes an instance. The generated constructor calls new Owner(data.owner) for every nested block and maps arrays of objects entry by entry, so instanceof answers true at any depth. The cost is bundle weight, since classes are real values while interfaces are erased. Use the class style when behaviour rides along with the data, and an interface when the shape is only documentation.

Why is my large ID field wrong after parsing?

Numbers in JSON parse into IEEE 754 doubles, so any integer above 9007199254740991 loses precision. Snowflake IDs from Discord and X, along with some database sequences, sit past that line, and the value your code reads back is not the value sent. TypeScript has no separate integer type, so nothing in the annotation warns you. If the API sends the ID as a string, keep it a string. If it sends a bare number, the fix belongs at the parse step with a reviver or a bigint aware parser, since by the time the object exists the digits are gone.

Can I paste an array at the top level?

Yes. A root level array is read as a list of records, every entry is merged into one shape, and the top level comes out as a type alias over the item type rather than a type of its own. The item type is named from the root name with the plural trimmed, so Rows produces Row and a Rows alias over Row[]. An array holding mixed shapes still works and comes through as a union, which the decision log flags as a spot worth checking.

Is my JSON uploaded anywhere?

No. Parsing, type inference and code generation all happen in JavaScript inside this page. Nothing is sent after the page loads, so a response carrying a customer record, an API key or a bearer token never leaves your machine. Nothing is stored between visits either, and closing the tab clears both panes.