utilyfe

JSON to TypeScript & Zod

Paste a real API response and get the TypeScript interface or Zod schema that matches it.

Runs entirely in your browser. Nothing you enter is uploaded or stored.

TypeScript
The generated code appears here.

How to use the JSON to TypeScript & Zod

  1. 1Paste a JSON object, array or response, or press "Try an example".
  2. 2Switch between TypeScript and Zod.
  3. 3Give the root type a name. It's used for the interface or the schema constant.
  4. 4Copy the result straight into your codebase.

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.

Questions

How does it decide a field is optional?
Only from an array of objects: if a key is missing from some elements and present on others, it's marked optional (field?: T in TypeScript, .optional() in Zod). A single object has no other sample to compare against, so every one of its fields is inferred as required: add ? by hand for a lone object if you know a field can be missing.
What happens when a field's value varies between array elements?
It becomes a union of every type actually seen: id: number | string if one record has a numeric id and another a string one. That's deliberately literal: it's telling you your data is inconsistent, which is usually worth knowing before you paste over it with a single type.
Why is the element type of an array called ItemsItem instead of just Item?
Because guessing a singular form from an English plural is unreliable: "status" and "data" don't pluralize the way "items" does, and getting it wrong silently would be worse than a slightly verbose but always-correct name. The rule is fixed and mechanical: the array's own key, plus "Item". Rename it after pasting if you'd rather have something shorter.
Does the Zod output create separate named schemas for nested objects?
No, nested objects are inlined as z.object({ ... }) within their parent, which is how most JSON-to-Zod output is written and keeps the whole schema readable as one block. TypeScript output does the opposite: every nested object gets its own named interface, which is the normal TypeScript convention.
Is my JSON uploaded anywhere?
No. Parsing and generating the types both happen in this browser tab, which matters when the payload is a real request or response with real data in it. Nothing is uploaded, logged or saved.