JSON to TypeScript interfaces, inferred from every record and not just the first
Paste a JSON response and get interfaces you can paste straight into a project. Arrays of objects are merged across every element, so a field that only some records carry comes out optional; a value that is null in one place and a string in another comes out as string | null; and an empty array becomes unknown[] rather than any[]. Nothing is uploaded and nothing is saved — the JSON you paste is gone when the tab closes.
- Nothing is uploaded or saved
- Works offline
Every element of every array is read, not just the first one. That is what makes a field missing from some records come out optional instead of being promised to code that will crash on record forty.
export interface Root {
team: string
'member-count': number
members: Member[]
}
export interface Member {
id: number
name: string
nickname: string | null
phone?: string
address: Address
tags: string[]
}
export interface Address {
city: string
postcode: string
}- 1 property is optional because it was missing from at least one object in an array.
- A property held null in one place and a value in another, so it is a union with null rather than being widened away.
- Quoted because they are not valid TypeScript identifiers: "member-count".
If the JSON needs tidying before you convert it, the JSON formatter and the JSON to YAML converter are on this site too.
How it works
- 1
Paste the JSON
A whole response, a single record, an array — all of them work, and conversion runs on every keystroke. Broken JSON gets a sentence naming the line and column rather than an empty box, and the one error shape that echoes your entire document back at you is trimmed down to the part worth reading.
- 2
Read the notes under the output
They say what the inference concluded and why: how many properties came out optional and what made them optional, whether a null forced a union, whether an empty array left an element type that could not be known, and which keys had to be quoted. A generated type you do not understand is a type you will fight later.
- 3
Name the root
The root type takes whatever name you type; everything nested takes its name from the key it sits under. An object under "address" becomes Address, and an array under "members" becomes Member[] — singularised, so you do not end up with Members[]. Two objects with identical shapes share one interface instead of being written out twice.
- 4
Choose the shape of the declaration
interface or type, readonly or not, exported or not. interface and type produce the same checking here; readonly marks every property and every array, which is what you want when the value came from a network response and nothing should be writing to it. Each of those controls lives in the output panel, because each changes the output and nothing else.
- 5
Copy it or download a .ts file
Copy puts the declarations on the clipboard. Download writes them to types.ts. Both give exactly what is on screen; there is no separate export path that could produce something different from the preview.
Frequently asked questions
Why read every element of an array instead of just the first one?
Because the first element is not the schema — it is one sample of it. Real responses leave optional fields out of some records, and a type generated from element zero promises a property that will not be there on element forty. TypeScript then happily compiles code that reads it, which is the worst possible outcome: you took the trouble to generate types and the compiler still lets the bug through. Press Sample above and look at "phone": it is present on the first member and absent from the second, and it comes out as phone?: string.
Why is a missing field written with a question mark rather than | undefined?
Because the two describe different things and the JSON only tells you one of them. A key that is absent from an object is what `?` means; a key that is present and holds undefined cannot exist in JSON at all, because undefined is not a JSON value. So `?` is the accurate reading. If your project runs with exactOptionalPropertyTypes, that distinction is one the compiler enforces, and the generated form is the one it wants.
What happens to null?
It is kept. A field that is null in one record and a string in another becomes string | null, not any and not string. Some converters widen a null away and the result is a type that lies about a value your code will eventually meet — usually at the point where something calls .trim() on it. In the sample above, "nickname" is null on the first member and "WM" on the second, and it comes out as nickname: string | null. Where a value is null in every record and never anything else, the only honest answer is null, and that is what you get.
Why is an empty array unknown[] and not any[]?
Because an empty array carries no information about its elements, and unknown is how TypeScript spells "a value whose type is not known yet". unknown[] forces whoever uses it to check before they touch an element; any[] silently switches off type checking for everything downstream of it, which is the same reason this codebase bans any outright. If you know what belongs in that array, one line of editing turns unknown[] into what it should be — and you will see it, which you would not with any[]. Non-empty arrays of mixed values are typed honestly too: a list holding strings and numbers comes out as (string | number)[], with the parentheses, because string | number[] would read as "a string, or an array of numbers", which is something else entirely. The members are listed in the order they were first met.
Exactly how are the interfaces named?
The root takes the name you type in the box. A nested object takes the PascalCase of the key it sits under. An array of objects takes the singularised key: users gives User[], categories gives Category[], addresses gives Address[], boxes gives Box[]. Words ending in -us, -ss or -is are left alone so status stays Status rather than becoming Statu. Two shapes that are byte-for-byte identical share a single interface. Two different shapes wanting the same name get a numeric suffix — Address and Address2 — rather than one quietly overwriting the other.
The singular of "movies" came out as "Movy". Is that a bug?
It is the rule doing exactly what it says, and it is a genuine limitation rather than an oversight. Turning -ies into -y is right for categories, companies, entries, properties and countries, and wrong for movies, cookies and series, and no mechanical rule separates those two groups — only an English dictionary does, and a dictionary is not something this page will download to fix a name. The rule is stated so its output is predictable. The root name is yours to type; a nested name that reads badly is a rename in your editor, and it costs one keystroke more than it should.
Why are some keys wrapped in quotes?
Because they are not valid TypeScript identifiers, and leaving them unquoted produces code that does not compile. JSON keys can contain anything — hyphens, spaces, a leading digit, an empty string — while an identifier cannot. So "member-count" comes out as 'member-count': number, which is valid and behaves identically; you read it as data["member-count"] rather than data.memberCount. Keys are never silently renamed to camelCase, because the property name has to match the JSON at runtime and a rename would produce a type that quietly does not describe the data.
Should I pick interface or type, and should I turn on readonly?
For object shapes the two are interchangeable in practice; interface can be reopened and merged by later declarations, type cannot, and type is what you need if you later want to build a union or an intersection from it. Pick whichever your codebase already uses. readonly is worth turning on for anything that arrived over the network: the object is a snapshot of a response, writing to it changes nothing on the server, and marking it readonly turns that mistake into a compile error. It applies to arrays too, so a property becomes readonly string[] rather than string[].
Do these types check the data at runtime?
No, and this is the most important thing to understand about them. TypeScript types are erased when the code is compiled; nothing survives into the running program to compare a response against an interface. Generating a type from one sample response tells the compiler what you expect, not what actually arrives — so a field that turns null next Tuesday will still be typed string, and the failure will happen in your code rather than at the boundary. Types generated here are a fast way to describe a payload accurately; if the payload has to be trusted, validate it at the boundary with a runtime schema check as well.
Does the JSON I paste go anywhere?
No. The parser and the inference both run in the JavaScript this page already loaded, there is no upload of any kind, and nothing is written to localStorage — closing the tab leaves nothing behind. That matters on this page in particular: the fastest way to get a type is to paste a real API response, and real responses carry customer records, internal hostnames and sometimes a token in a header field somebody copied along with the body.