Why Generate TypeScript From JSON?
Every TypeScript project that talks to an HTTP API hits the same wall: the API returns JSON, but your code wants strongly-typed objects. Writing those interface declarations by hand is tedious, error-prone, and goes stale the moment the backend adds a field. Generating them from a real JSON sample fixes all three problems — the shape matches the actual payload, every field is captured, and regenerating takes a second when the API evolves.
Example: API Response to Interface
Given this JSON response from a GET /users/42 endpoint:
{
"id": 42,
"name": "Ada Lovelace",
"email": "[email protected]",
"verified": true,
"roles": ["admin", "editor"],
"profile": {
"bio": "Mathematician",
"avatar": null,
"website": "https://ada.dev"
},
"created_at": "2026-10-03T12:00:00Z"
}The generator produces:
export interface Profile {
bio: string;
avatar: string | null;
website: string;
}
export interface User {
id: number;
name: string;
email: string;
verified: boolean;
roles: string[];
profile: Profile;
created_at: string; // ISO 8601
}Interface vs Type Alias — Which to Pick
TypeScript gives you two ways to describe an object shape, and the choice matters more than most devs realise.
- interface — open for declaration merging. You can re-open the same interface in another file and add fields. Good for public DTOs and anything other packages may need to extend. Supports
extendsfor inheritance. - type — closed, but more flexible. Supports unions (
type Status = 'active' | 'banned'), intersections (type A & B), mapped types, conditional types, and utility types. Picktypewhen the shape is final and you need any of these features.
Rule of thumb for JSON-derived shapes: start with interface. Switch to type only when a discriminated union is required (e.g. a kindfield with multiple variants).
How Optional Fields Are Inferred
Array-of-object JSON is the main source of optional fields. If a key appears on some items but not others, it's optional:
// Input
[
{ "id": 1, "name": "Ada" },
{ "id": 2, "name": "Linus", "admin": true }
]
// Output
export interface Item {
id: number;
name: string;
admin?: boolean; // optional — missing on item 1
}A field that is null in the sample becomes T | null. When a field is both missing on some items and null on others, the output is name?: string | null— this matches TypeScript's exactOptionalPropertyTypes flag under strict mode.
Union Types From Mixed Arrays
Arrays with mixed primitive types become unions:
// Input: { "values": [1, "two", true, null] }
export interface Root {
values: (number | string | boolean | null)[];
}For arrays of objects with different shapes, the generator intersects the fields — common fields are required, divergent fields become optional. If you need a discriminated union instead (one type per variant, tagged with a kindfield), convert the output manually — it's a design decision the generator can't make for you.
Handling Dates and Enums
JSON has no native date type — dates come across as strings ("2026-10-03T12:00:00Z"). The generator types these as string. If you want Date, you have two options:
- Keep the type as
stringand addnew Date(user.created_at)at use sites. - Define a parse helper that returns a stronger type:
type ApiUser = Omit<User, 'created_at'> & { created_at: Date }.
For enums (fields that are always one of a known set), rewrite the generated string as a literal union manually: role: 'admin' | 'editor' | 'viewer'. The generator can't infer this because it only sees one sample.
Common Pitfalls
- Empty arrays become
any[]. Feed a sample with at least one element. - All-
nullfields becomenull. Needs a non-null sample to infer the real type. - Snake-case fields stay snake-case. TypeScript convention is camelCase, but renaming would break the JSON contract. Keep snake-case and use a transformer at the API boundary, or write a mapped type to rename programmatically.
- Numbers lose precision beyond 2^53. If your API returns 64-bit integers (IDs, timestamps in ns), consider the
biginttype or keep them as strings in both the API response and the TypeScript interface.
Alternative: Schema-First With Zod
Static interfaces catch typos but don't validate at runtime — if the API lies, your code crashes deep in a component. For external APIs where you can't trust the shape, pair the generated interface with a Zod schema:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
verified: z.boolean(),
roles: z.array(z.string()),
});
export type User = z.infer<typeof UserSchema>;
// At the API boundary:
const user = UserSchema.parse(await res.json());z.infer gives you the same static type as the generator, plus runtime validation. For internal APIs where you control both ends, the static interface alone is usually enough.
Related JSON Tools
- JSON Formatter — the parent tool with interactive input and real-time validation.
- JSON Schema Validator — validate payloads against a JSON Schema.
- JSON to YAML — convert config files between formats.
- JSON Tree Viewer — explore nested structures visually before generating types.