What it’s for
An API response is usually the most accurate spec you have of its own shape: more accurate than a wiki page, and definitely more accurate than guessing. This turns one into a TypeScript interface or a Zod runtime schema, so a shape you already have doesn’t have to be typed out field by field by hand.
TypeScript interfaces are compile-time only: free, but they don’t check anything once the code is actually running. Zod schemas are runtime validators that also produce a TypeScript type: MySchema.parse(response) throws if the real response doesn’t match, which is the piece plain interfaces can’t do. Paste the same JSON and switch between them to see both.
Reading an inferred type
export interface Order {
id: string;
total: number;
customer: Customer;
items: ItemsItem[];
}
export interface Customer {
name: string;
email: string;
}
export interface ItemsItem {
sku: string;
quantity: number;
note?: string;
}
Every nested object gets its own interface, named after the key that held it (customer → Customer). An array of objects gets an element interface named after its own key plus “Item”: mechanical and always predictable, rather than a pluralisation guess that’s occasionally wrong. note? is optional because the sample array had it on one item and not the other; every other field was present on both, so it’s required.
Merging an array of objects into one shape
Real API arrays are rarely perfectly uniform: an optional field, a value that’s sometimes null, an id that’s a number on old records and a string on new ones. Rather than picking one element and ignoring the rest, every element in the array contributes to one merged shape:
- a field present on every element stays required
- a field missing from some elements becomes optional
- a field whose value varies in type becomes a union:
id: number | string - a field that’s null on some elements and a value on others becomes
T | null
That merged shape is what both the TypeScript interface and the Zod schema are built from, so the type you get reflects everything in your sample, not just its first entry.
Null versus optional versus missing
These are three different things, and the output keeps them apart on purpose:
| In your JSON | TypeScript | Zod |
|---|---|---|
| Always present, never null | field: T |
field: z.T() |
Present but always null |
field: null |
field: z.null() |
| Sometimes present, sometimes missing | field?: T |
field: z.T().optional() |
Sometimes null, always present |
field: T | null |
field: z.T().nullable() |
Sometimes null, sometimes missing |
field?: T | null |
field: z.T().nullable().optional() |
A field that’s always exactly null in your sample genuinely can’t be typed as anything more specific: that’s an honest reflection of what the sample shows, not a bug in the inference.
What it won’t guess
It infers a shape from the sample you gave it: it doesn’t know your business rules. It won’t turn a string that happens to look like a date into Date, won’t notice that an id is always a UUID, and won’t assume a field is required just because it happened to be present in every element of a short sample. Treat the output as a solid first draft, not a final schema, especially from a small sample, where a field that’s always present today might genuinely be optional in a case you didn’t happen to include.