You have an API response and you need TypeScript interfaces for it. Writing them by hand is tedious and error-prone — miss one optional field and you get a runtime error the type checker promised you could not have.
The JSON to TypeScript generator does the mechanical part. Understanding what it can and cannot infer is what makes the output trustworthy.
What happens to your sample
Every value gets a shape. Shapes of array items are merged with each other, so a 10,000 element array of users produces one User interface rather than ten thousand. Identical shapes are de-duplicated, so an Address object appearing in three places becomes one interface referenced three times.
{
"user": {
"id": 1001,
"name": "John Doe",
"email": "john@example.com",
"active": true,
"roles": ["admin", "user"],
"address": { "city": "Chennai", "country": "India", "zip": "600001" }
},
"projects": [
{ "id": 1, "name": "Website Redesign", "status": "active" },
{ "id": 2, "name": "Mobile App", "status": "completed" }
]
}export interface Root {
user: User;
projects: Project[];
}
export interface User {
id: number;
name: string;
email: string;
active: boolean;
roles: string[];
address: Address;
}
export interface Address {
city: string;
country: string;
zip: string;
}
export interface Project {
id: number;
name: string;
status: string;
}How optional and nullable are decided
- A field present in some objects of a shape but not others becomes optional:
email?: string. - A field that is sometimes
nullbecomes nullable:lastLogin: string | null. - A field that is both becomes
lastLogin?: string | null. - A field with genuinely mixed types widens to the safest common type — and this is worth investigating rather than accepting.
All three depend entirely on the sample containing enough variety. One record cannot tell the generator anything about optionality.
Use a good sample
The single biggest determinant of output quality. A good sample:
- has arrays with several items, not one
- includes records where optional fields are absent
- includes at least one
nullwhere nulls occur - covers the empty state — an empty
data: []array tells the generator nothing about item shape, so include a populated one
Before generating, run the sample through the analyzer. It reports exactly which fields are missing from some records and which change type — which is the same information the generator is using, presented so you can sanity-check it.
Where generated types fall short
- String unions.
status: stringshould probably bestatus: "active" | "completed". Inference cannot know the closed set; you have to write it. - Branded types. An
id: numberand aparentId: numberare interchangeable to the type checker. Branding them prevents a whole class of bug. - Dates. Timestamps arrive as strings. Decide whether your model holds
stringorDateand convert at the boundary. - Empty arrays and empty objects. Nothing can be inferred from them; they surface as
unknown[]orRecord<string, unknown>.
Types are not validation
This is the important part. TypeScript interfaces are erased at build time. JSON.parse returns any, and asserting as User tells the compiler to stop checking — it does not make the claim true. If the API sends something else, you get a runtime error somewhere far from the cause.
For data crossing a trust boundary, validate at runtime: a JSON Schema checked by a schema validator, or a runtime validator such as Zod or Valibot that can also produce the static type. Then the type and the check cannot drift apart.
Other languages
The same generator emits Python dataclasses, Java classes, Kotlin data classes, Go structs with JSON tags, C# classes with System.Text.Json attributes, Rust structs with serde derives, PHP typed classes and JSDoc typedefs. Where a language convention renames fields — snake_case in Python and Rust, PascalCase in Go and C# — the mapping annotation is emitted so serialization still matches the original keys.