JSON to TypeScript
The result appears here.
Generate
Your document never leaves the browser.
TypeScript interfaces from a JSON sample — written as you type, in your browser.
Paste an API response, a fixture or a config file and the interfaces for it appear beside it. Every record in the document is read, not just the first, so a key that only some records carry comes out optional rather than missing.
Each nested object gets an interface of its own, named after the key that holds it: customer gives Customer, and the elements of items give Item, in the singular. The root is named after the file, so order.json gives Order; a pasted document has no file name, so its root is Root until you type another name in Root name.
Look at the two line items. note is present in both, once null and once a string, so it stays required and reads null | string. gift-wrap appears in only one, so it gets a question mark, and because it is not a valid identifier the key is written in quotes rather than renamed. That matters for TypeScript: the interface describes the wire format, and a renamed key would describe a different object.
The sample, an order with two line items, pasted as the source
{
"order_id": 1042,
"placed_at": "2026-09-25T10:15:00Z",
"paid": true,
"customer": { "name": "Ada Lovelace", "email": "ada@example.com" },
"items": [
{ "sku": "PEN-01", "qty": 2, "price": 3.5, "note": null },
{ "sku": "INK-07", "qty": 1, "price": 12, "note": "Fragile", "gift-wrap": true }
]
}The interfaces written for it, at the default settings
export interface Customer {
name: string;
email: string;
}
export interface Item {
sku: string;
qty: number;
price: number;
note: null | string;
"gift-wrap"?: boolean;
}
export interface Root {
order_id: number;
placed_at: string;
paid: boolean;
customer: Customer;
items: Item[];
}The sample, with Declaration type alias
export type Customer = {
name: string;
email: string;
};
export type Item = {
sku: string;
qty: number;
price: number;
note: null | string;
"gift-wrap"?: boolean;
};
export type Root = {
order_id: number;
placed_at: string;
paid: boolean;
customer: Customer;
items: Item[];
};Option
Declaration: interface, the default, or a type alias. The two describe the same shape; unlike an interface, a type alias cannot be merged with a later declaration of the same name, which some codebases prefer.
Option
Optional fields: name?: T, the default, or name: T | undefined, which keeps every key in the type and lets its value be undefined instead.
Option
export declarations: on by default, so the file can be imported as a module. Turn it off to paste the types into an existing file.
Two objects with the same keys and types, wherever they sit in the document, become one interface. An array holding strings and numbers is typed (string | number)[], and a key seen only as null is typed null, since nothing else is known about it.
TypeScript has one number type, so 3.5 and 12 are both number here. An integer larger than 2^53, such as a 20-digit identifier, is typed number too, but JSON.parse rounds it on the way in; if your API sends such ids, ask for them as strings.
Two addresses of one shape, a mixed list, a null and a 20-digit id
{
"billing": { "city": "Paris", "zip": "75001" },
"shipping": { "city": "Lyon", "zip": "69001" },
"tags": ["new", 3],
"coupon": null,
"id": 12345678901234567890
}One interface for both addresses, and number for the id
export interface Billing {
city: string;
zip: string;
}
export interface Root {
billing: Billing;
shipping: Billing;
tags: (string | number)[];
coupon: null;
id: number;
}A sample shows what was, not what may be. A status string that is always "paid" in your sample is still string, not a literal type, and a date arrives as string because JSON has no date type. Widen or narrow the interfaces where you know more than the sample does.
A status that is always "paid", and a timestamp
{ "status": "paid", "paid_at": "2026-09-25T10:15:00Z" }Both are typed string
export interface Root {
status: string;
paid_at: string;
}Inference and code generation both run inside this browser tab. The sample is never uploaded, stored on a server or logged, because the page has no server to send it to, and once loaded it carries on working with the connection off.
Read next:
Every record. Their keys are merged, so a key missing from any one of them is optional in the result.
Yes, with the JSON to Zod page or the Zod tab. It writes runtime validators and exports the matching types with z.infer.
A pasted document has no file name to take one from. Type a name in Root name, or open the file, and the root takes its name.