JSON to TypeScript
Generate TypeScript interfaces from JSON: merges array elements, marks optional keys, unions mixed types, and keeps null distinct — all in your browser.
export interface Root {
id: number
name: string
active: boolean
address: Address
roles: string[]
posts: Post[]
tags: unknown[]
}
export interface Address {
city: string
zip: null
}
export interface Post {
id: number
title: string
views: number | string
pinned?: boolean
}
Interfaces: 3
Types inferred from data, not guessed
A JSON document does not carry its own types — it carries values, and the types have to be read back out of them. That is easy for a single object and surprisingly subtle for a collection: the shape you want is not the shape of any one record, but the shape every record has to satisfy. This tool infers TypeScript interfaces from a JSON sample by looking at everything the sample contains, so the types it produces describe the whole of your data rather than the first row that happened to be on top.
The conversion goes one way only. Turning types back into JSON would mean inventing values, and the point here is the opposite: the pasted document is the source of truth, and every declaration is derived from it. Paste a payload from an API, a config file, or a log line, and read off the interface you would otherwise have written by hand.
How arrays are merged
The interesting decisions all happen at arrays. A naive converter looks at the first element and stops, which makes every field it happens to see look required and misses every field it does not. This tool instead merges all the elements into one type, and three things fall out of that:
- A key present in some elements but missing from others becomes optional, written with a question mark. If half your records have a "middleName" and half do not, the field is "middleName?", which is exactly what a consumer has to handle.
- A key whose value differs in kind across elements becomes a union. A field that is a number in one record and a string in another is typed "number | string" — not because that is tidy, but because that is what the data actually contains and what your code has to accept.
- A nested object inside the elements is merged the same way, recursively, and pulled out into its own interface. Ten array elements each carrying an "address" produce one Address interface describing all ten.
When the document is itself an array at the top level, the root becomes an alias to an item interface — for example "type Root = RootItem[]" — with the merged element type written out below it.
Null, optional, and why they are different
It is tempting to treat a null the same as a missing key, and wrong. In TypeScript "name?: string" means the property may be absent; "name: string | null" means it is always there but may hold null. Those are different contracts, and a consumer checks them differently — "in" versus a value comparison. This tool keeps them apart: an explicit null in the data becomes a "| null" union member, ordered last so "string | null" reads the way you expect, while a key that is simply absent from some records becomes optional. A field that is both — null in one record, missing in another — comes out as both, "field?: T | null", because both facts are true of your data.
Nested objects become named interfaces
Rather than inline a nested shape into its parent, each object is extracted into its own interface whose name is derived from the key it sits under. A "user" object becomes a User interface; an "address" inside it becomes an Address interface the User refers to. Deeply inlined types are hard to read and impossible to reuse, and named interfaces are what you would have written yourself. Array elements are singularised when they can be — "users" yields a User, "categories" a Category — and a key that does not pluralise gets an Item suffix so the element still has a name of its own.
If two different objects would take the same name — two unrelated "data" objects, say — the second is suffixed rather than merged, so distinct shapes stay distinct. The root object is emitted first and you can rename it; the choice between "interface" and "type" output is a toggle, since a few codebases prefer type aliases throughout.
The unknown fallback
Some values carry no type information at all. An empty array could hold anything; an empty object has no keys to describe. Rather than reach for "any" — which switches type-checking off for everything downstream — the tool falls back to "unknown": an empty array becomes "unknown[]", an empty object becomes "Record<string, unknown>". The difference matters. "any" silently lets bugs through; "unknown" forces the consumer to narrow the value before using it, so the inferred type stays honest about what the sample did and did not tell you.
These are the types you would tighten by hand once you know what the empty collection is meant to hold — but until the data says so, "unknown" is the truthful answer, and it is the one that keeps the rest of your types safe.
What it runs on, and where
Everything happens in your browser. The JSON is parsed and the types are inferred on your own device; nothing you paste is uploaded, stored or logged. That makes the tool safe to use on a real API response or a config file with secrets in it — the sample never leaves the page. The output is plain TypeScript you can paste straight into a "d.ts" file or a module, adjust the few "unknown" fields the data could not describe, and use.
Frequently asked questions
- Is my JSON sent to a server?
- No. The document is parsed and the types are inferred entirely in your browser, and nothing you paste is uploaded or logged. It is safe to use on a real API response or a config file.
- Why is a field optional when it is present in my sample?
- Because it is absent from at least one element of an array the tool merged. The inference reads every element, not just the first, so a key that some records omit becomes optional — that is the type your data actually supports, even if the record you looked at happened to include it.
- Why did a field become a union like string | number?
- Because the value had different kinds in different array elements — a string in one record and a number in another. The merged type has to accept both, so it is written as a union. If that surprises you, it usually means the data is less uniform than expected, which is worth knowing.
- Why does the tool use unknown instead of any?
- For values it cannot describe — an empty array, an empty object — "unknown" keeps the result type-safe, forcing a consumer to narrow the value before using it, whereas "any" would switch type-checking off. You can tighten those fields by hand once you know what the empty collection holds.
- What is the difference between the interface and type output?
- None in the types they describe — both produce the same shapes. "interface" is the common idiom for object types and can be extended and merged; "type" aliases are what some codebases prefer to use throughout. The toggle is there so the output matches your project’s style.
- Can it turn TypeScript back into JSON?
- No, and deliberately. The conversion is one-way: JSON in, types out. Going the other direction would mean inventing values that were never in your data, and the whole point is that every declaration is derived from what you actually pasted.
- How are nested objects named?
- From the key they sit under: a "user" object becomes User, an "address" inside it becomes Address. Array elements are singularised where possible — "categories" gives Category — and a key that does not pluralise gets an Item suffix. Two different shapes that would collide on a name are suffixed rather than merged, so they stay distinct.