Skip to main content

JSON to TypeScript

JSON to TypeScript generates interfaces from a sample document in your browser. Paste every record you have rather than one: a field that appears in some records and not others is only detectable across samples, and it becomes an optional member rather than a required one that is missing at runtime.

Local-only
Shape
Sample JSON
0 B1 lineLn 1, Col 1
Syntax error at line 1, column 1

Empty JSON input (truncated document)


^
Generated coderead-only
0 B1 line
Paste more than one record if you have them. A field present in some records and absent from others is only detectable across samples, and it is the difference between a type that matches your data and one that matches your first row.

How does JSON to TypeScript work?

The generator builds one intermediate type model from your sample and then renders it, which sounds like an implementation detail and is the reason the output is correct. The genuinely hard decisions — is this field optional, are these two objects the same type, what is the element type of a mixed array — are answered once, in one place, rather than re-derived every time a value is encountered.

Optional fields come from disagreement between records

Given [{"a":1}, {"a":1,"b":2}], the obvious implementation reads the first element and produces { a: number }, silently losing b. The correct answer merges the field sets of every element: a is in both so it is required, b is in one so it is b?: number. This is the single largest difference between generators, and it is why pasting one record produces a type that describes one record rather than your API.

Identical shapes become one type, not fifty

Every object is fingerprinted by its structure — key names, optionality, and the fingerprint of each value type, order-independent. A document with fifty customers, each holding an address of the same shape, produces one Address interface rather than Address through Address50. Names come from the key that held the object, singularised when it held an array, so "users": [{…}] generates User.

null is nullability, not a type

When a field is a number in one record and null in another, the output is number | null rather than a union type with a generated name. When a field is null in every record the honest answer is unknown with a comment saying so — emitting null would be accurate about your sample and useless about your data.

Numbers are numbers, with a note

TypeScript has one numeric type, so integers and floats both become number. An integer too large for a 32-bit type is still flagged in a comment, because the same model drives the Go, Java and C# generators where that distinction decides between int and int64 — and because a 64-bit ID arriving in JavaScript at all is worth knowing about.

Sample

[
  { "id": 1, "name": "Ada", "deleted_at": null },
  { "id": 2, "name": "Grace", "deleted_at": null, "team": "ops" }
]

Generated

export interface User {
  id: number;
  name: string;
  deleted_at: unknown; // only null in the sample
  team?: string;
}

What options and edge cases does JSON to TypeScript support?

Options and inference rules
ParameterTypeDefaultBehaviour & edge cases
Root typeidentifierRootName for the top-level type. An array root is reported as Name[], and object shapes inside it are named after the keys that hold them.
interface / typedeclarationinterfaceInterfaces support declaration merging and produce better error messages on object shapes; type aliases are required if you later want unions or mapped types. Either is valid here.
readonlybooleanoffMarks every member readonly. Worth turning on for a parsed API response, which nothing in your code should be mutating in place.
camelCase keysbooleanoffRenames snake_case keys and records the original in a trailing comment. Only correct if something in your pipeline actually renames them — the generated type otherwise stops matching the JSON.
Optionalityinferredacross recordsA key absent from at least one record of the same shape becomes optional. Paste several records; one record cannot express this.
Nullableinferredunion with nullA value that is null in some records and a string in others becomes string | null. Absent and null are different things and are reported differently.
Mixed arraysunionparenthesisedAn array of mixed scalars becomes (string | number)[]. The parentheses matter: string | number[] means something else entirely.
Input sizebytes2 MBInference reads every value. A few hundred representative records describe a shape as well as a million, so the ceiling costs nothing in practice.

Frequently asked questions

Why is a field optional when my API always sends it?

Because at least one record in the sample you pasted did not have it. The generator can only describe the data it was given. If the field is genuinely always present, paste a sample where it always appears — or remove the ? by hand, which is the right call when you know the contract better than the sample does.

Should I paste one record or the whole response?

The whole response, and ideally several. Optionality, nullability and mixed types are all properties of the set of records, not of any single one. A generator fed one record cannot distinguish a field that is always present from one that happened to be present that time, and it will confidently produce a type that is wrong in the way that is hardest to notice.

Why did I get unknown instead of a real type?

Every sample at that position was null, so there is nothing to infer from. The alternatives are worse: null as a type is accurate about the sample and useless about the data, and any turns off type checking for everything downstream of it. unknown forces you to narrow before use, which is the correct outcome when the type is genuinely unknown.

Can it generate runtime validators, not just types?

No. TypeScript types are erased at compile time, so an interface generated from a sample tells you nothing about the response you actually received at runtime. If you need the guarantee rather than the annotation, generate a JSON Schema instead and validate against it, or hand-write a Zod schema. Treating a generated interface as validation is a common and expensive mistake.

Does it handle deeply nested or recursive documents?

Nesting, yes, to a depth of 100 — past that the generated code would be unusable anyway, so there is a clear ceiling rather than a stack overflow. True recursion (a comment that contains comments) is inferred as far as the sample goes; if the sample has three levels you get three levels, because nothing in the data says the structure repeats forever.

Is my JSON uploaded?

No. Inference and rendering both run in the tab you already have open — open DevTools and watch the Network panel while you paste. This matters more here than on most tools, because the documents people convert are production API responses, often with real customer records in them.