json

FormatValidateConvert

JSON to TypeScript

JSON
Language
Structured output

Result· read-only

—TypeScript

The result appears here.

Generate

JSON to TypeScript: generate interfaces from JSON.

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.

3

options: declaration, optional fields, export

2^53

past it, JSON.parse rounds a number

0

bytes leave your machine

What the sample becomes

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.

1

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 }
  ]
}
2

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 three options

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

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

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

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.

Shapes the inference settles for you

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.

1

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
}
2

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;
}

Check these by hand

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.

1

A status that is always "paid", and a timestamp

{ "status": "paid", "paid_at": "2026-09-25T10:15:00Z" }
2

Both are typed string

export interface Root {
  status: string;
  paid_at: string;
}

Your sample stays here

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:

FAQ

Frequently asked questions

Didn’t find your answer?Write to us on the contact page →
Does it read every record in an array, or only the first?

Every record. Their keys are merged, so a key missing from any one of them is optional in the result.

Can I generate Zod schemas instead?

Yes, with the JSON to Zod page or the Zod tab. It writes runtime validators and exports the matching types with z.infer.

Why is my root interface called Root?

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.

Keyboard shortcuts

Send feedback

Questions, bug reports and feature requests are all welcome. A bug report is easiest to act on with the shape of the document that caused it — never send anything confidential.

Email us

Contact page, in a new tab, so this page stays as it is.

Settings

Indent

The result is written with it — YAML and XML at 2 spaces when it is Tab.

Code text size
14 px

Both panes.

Wrap long lines

Load from a URL

Your browser fetches it directly — the request goes to that site, never to us.