DevKitHub

Programming

JSON to TypeScript — Interfaces, Types and Zod Schemas

Paste a JSON response to get TypeScript types for it, as interfaces, type aliases or a Zod 4 schema. Every element of an array is merged into one type, so optional and nullable fields come out right.

21 lines
20 lines
  • $.next_cursor: null in every sample, so the type is unknown. Replace it with the real type, such as string | null.

The types describe the sample you paste, so they are only as complete as it is: paste an array of several responses to find the optional keys. Zod output is written for Zod 4.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

Types are inferred by merging, and that is where generators differ. Every value found at the same position, each element of an array and each sample of a field, is added into one description of that position. So [{"id":1,"email":"a@x.io"},{"id":2,"email":null,"admin":true}] gives one type in which email is string | null and admin is optional, admin?: boolean; a generator that reads only the first element leaves admin out, and one that reads only the second makes it required. A key whose values differ becomes a union, such as number | string, or string | Owner when it is an object in one sample and a string in another. An empty array is unknown[] unless another sample at the same position has elements, a key that is null in every sample is unknown, and an object with no keys is Record<string, unknown>, each with a warning, because the sample says nothing about the real type.

Each nested object becomes a named type, in PascalCase from its key: billing_address, billing-address and billingAddress all give BillingAddress. Array elements are named by a deliberately small rule, applied once per key: children and people become Child and Person, -ies becomes -y, -sses, -shes, -ches and -xes lose the -es, and any other final -s is dropped unless the word ends in -ss, -us or -is. A name the rule leaves unchanged gets Item added, so data gives DataItem. It is a rule, not a dictionary, and it gets some words wrong (movies gives Movy), so rename those. Two positions with structurally identical shapes share one declaration, so a user’s address and a company’s address are both Address; two different shapes that want the same name are numbered, Address and Address2, with a warning. Keys that are not identifiers, such as content-type or 2fa, are quoted.

JSON has one number type and no date type, and the output claims no more than the JSON does. Numbers are number, integers beyond 2^53 included, which JSON.parse rounds; those are named in a warning. Dates are string, because that is what JSON.parse returns, and a Date type would be a promise the runtime does not keep. The Zod output is for Zod 4: z.object, z.array, z.union, .nullable(), .optional() and a z.infer type for each schema, declared so no schema is used before it exists. Only there are strings checked for a format: where every sample of a field is an ISO 8601 date-time or date in exactly the form Zod accepts, it becomes z.iso.datetime(), with { offset: true } if any sample has an offset, or z.iso.date(). Readonly marks every property and array readonly, and in Zod adds .readonly(), which also freezes the parsed value. All of it describes the sample: a key present in every element you pasted comes out required, whatever the API documentation says.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

Object keys must be strings in double quotes, as in {"name": 1}.

{id: 1, name: 'Ada'}
Why:
A JavaScript object literal rather than JSON, usually copied from console.log output or from source code. It looks like JSON, but JSON requires double-quoted keys and strings.
Fix:
In the browser console run copy(JSON.stringify(value, null, 2)) and paste that, or quote the keys and strings by hand.

Trailing comma: JSON does not allow a comma after the last member of an object.

{
  "id": 1,
  "name": "Ada",
}
Why:
A comma after the last member, which JavaScript and TypeScript allow and JSON does not. Hand-trimmed responses end up with one.
Fix:
Delete the comma before the closing brace or bracket.

JSON does not allow comments.

{
  // the user id
  "id": 1
}
Why:
Comments, as in tsconfig.json or VS Code settings. Those files are JSONC, a superset; JSON itself has no comments.
Fix:
Remove the comments before pasting.

A field the API sometimes leaves out came out required.

Why:
Every sample you pasted happened to include it. A key is optional only when some element at the same position lacks it, and a single response cannot show that.
Fix:
Paste an array of several responses, including one without the field, so it is missing from at least one element.

A field came out as unknown[] or unknown.

Why:
The array was empty, or the value was null, in every sample, so there was nothing to infer its type from. A warning names each such field.
Fix:
Add a sample where the array has elements or the value is set, or write the type in by hand, such as string | null.

Frequently asked questions

How do I get optional fields right?
Paste an array of several real responses rather than one. Every element is merged into one type, and a key missing from any element becomes optional. From a single response every key looks required, because nothing shows otherwise.
Why are dates typed as string and not Date?
Because JSON has no date type and JSON.parse returns the string. A Date in the type would compile and then fail at run time the first time you call a method on it. Convert the strings yourself, or in the Zod output add a transform; the Zod output already checks the format with z.iso.datetime() or z.iso.date() when every sample matches.
Which version of Zod does the schema need?
Zod 4, where z.iso.datetime() and z.iso.date() are the date checks and z.string().datetime() is deprecated. The rest, z.object, z.array, z.union, z.record(z.string(), z.unknown()), .nullable(), .optional() and z.infer, is written the same way in Zod 3, so for Zod 3 replace the z.iso calls with z.string().
Should I use interfaces or type aliases?
For object shapes they are interchangeable here. Interfaces can be extended and merged by later declarations; type aliases can also name unions. A root that is an array or a primitive is always written as a type alias, because an interface cannot be one.

Last updated