JSON to TypeScript types
Paste an API response or a .json file: out come the TypeScript interfaces (or type aliases) and the JSON Schema 2020-12, with optional fields worked out from every element of a list, not just the first. The JSON never leaves your browser.
Why looking at the first element is not enough
The JSON that comes from an API almost never has all its objects alike. In a list of orders the first one has notes and the second does not; the third has a tracking code because it has already shipped. A generator that only looks at the first element writes a type with compulsory notes and no tracking: the code compiles, and then halfway through a run you find undefined where the type promised a string.
Here every element of every list is read, lists inside lists included. A key missing from even one object becomes optional (tracking?: string). A field that is now a number, now a string becomes number | string, and the page flags it with how many times each type turns up, because it is often dirty data: a price written once between quotes.
Being missing and being null are not the same thing, and the type says so. A key that is sometimes absent gets the question mark; a key that is always there but sometimes null gets | null; if both happen, it gets both, and with --strict TypeScript treats the two cases differently.
The choices you will find in the result
A list that is always empty becomes unknown[] and not any[]. Nothing is known about its elements, and unknown makes you check them before using them, whereas any would switch the check off exactly where it is needed. An object that is always empty becomes Record<string, unknown> for the same reason.
Keys that are not valid names stay between quotes: "date-of-birth", "first name", "2fa". Reserved words like default or class, on the other hand, are allowed as property names and stay without quotes.
The names of nested interfaces come from the key: shipping_address becomes ShippingAddress. For the elements of a list the name goes into the singular, a declared heuristic that looks at how the word ends in Italian, English and Spanish (utenti gives Utente, categories gives Category, direcciones gives Direccion). Endings that can mean two things are left alone: Italian -umi comes from both volume and consumo, so legumi stays Legumi, while the most common words are written out one by one (consumi gives Consumo). What the ending cannot settle is the same word in two languages: roles is role in English and rol in Spanish, and English wins. That is why every name put into the singular is listed under the result.
Two objects with the same shape become one single type: if the shipping and billing addresses have the same keys, out comes Address, used twice. If the names have nothing in common, like author and category which are both an id and a name, the second stays as an alias, type Category = Author, because category: Author would read wrong. And a name that matches a type TypeScript or the browser already has (Date, Event, Location, AudioData) gets the parent's name in front, so as not to shadow the real one in the whole file.
interface or type: which one to pick
For describing data the two forms are equivalent: pick the one your project already uses. Interfaces can be extended and are what the TypeScript documentation recommends for objects; type aliases are needed when the type is not an object, so a root that is a list or a plain value comes out as a type alias even if you chose interface, for example export type Root = RootItem[];, and the page says so. Everything comes out with export.
JSON Schema, to check the data when it arrives
TypeScript types vanish when the code runs: they protect whoever writes the program, not the program from the data it receives. To check a response when it arrives you need a schema, and the second output is a JSON Schema draft 2020-12, read by Ajv in JavaScript, jsonschema in Python and most tools that validate configurations and APIs.
The schema mirrors the types: the same interfaces sit in $defs, the compulsory fields in required, nulls and mixed fields in a type with several values. It tells integer from number by the way the number is written: 10 is an integer, 10.0 is not, because whoever writes the point is saying that decimals may arrive there.
The closed schema box adds additionalProperties: false: a key that was not in the sample becomes an error. Switch it on for data you produce yourself; leave it off for somebody else's API, which tomorrow may add a field and get good responses rejected.
Numbers too big and repeated keys
In JavaScript a whole number is exact only up to 9,007,199,254,740,991, that is 2 to the 53rd minus 1. Beyond that the last digits change by themselves: 9007199254740993 read with JSON.parse becomes 9007199254740992, without a word. It happens with identifiers, which is why some APIs send the id as a string too. This page reads the JSON with a reader that keeps the original digits, and tells you where such a number is and what it turns into. In the type it stays number; the advice is to have it sent as a string.
Same for repeated keys in the same object: JSON.parse keeps the last one without warning, and here they are flagged with the line. And a broken JSON gets line, column and the reason in words: the extra comma before a brace, single quotes, a comment, a True written the Python way.
How it was tested
A type generator can go wrong in a way you cannot see: types that are all optional, or all any, always compile. So the test takes hundreds of randomly generated JSON files (some with the same shape repeated in several places, each with its own variant) and has the TypeScript compiler, with --strict, build the produced types together with the JSON itself. Then it removes a compulsory field, changes the type of a value or adds an invented key, and demands that the compiler reject the file.
The schema is checked by a library that is not ours, jsonschema for Python: the starting JSON must be valid, the broken version must not. The JSON reader is compared with Python's on thousands of texts mangled on purpose and gives the same answers, with two deliberate differences: it tolerates the invisible BOM some Windows programs put at the start of a file, and it rejects NaN and Infinity, which do not exist in JSON.
What it does not do
It does not guess meanings. A date stays string, and so does an email: from the text you cannot know whether a different format will arrive tomorrow. Likewise the status of an order, with three values seen, stays string and does not become the list of those three: the allowed values are yours to write.
It does not recognise dictionaries. An object that uses identifiers as keys becomes an interface with those keys; if it is really a map, change it to Record<string, User>. And it does not invent recursive types: a tree becomes nested interfaces as deep as the sample.
It does not write validation code or classes, and it does not repair broken JSON: for that there is Repair JSON, and to lay it out again and see where it breaks there is JSON formatter & validator. If you need a table out of a JSON there is JSON to CSV, and if the file is YAML, like a docker-compose or a Kubernetes manifest, go through YAML to JSON.