JSON → TypeScript: 5 ways to generate types, and when to use each
From quicktype to manual hand-rolling to runtime validators like Zod and io-ts — the trade-offs of every approach to turning JSON into TypeScript types.
"Take this JSON, give me a TypeScript interface for it" is one of those tasks that sounds trivial and turns out to have five real answers, each with different trade-offs. Here is the spectrum.
1. Hand-write it
For small, stable payloads — auth responses, configuration files, the result of a single REST endpoint — typing it by hand takes a minute and gives you the most precise types you will ever have.
The cost is maintenance. The moment the upstream payload changes and nobody updates the interface, you have lying types. For schemas that change frequently or come from a system you do not control, this is usually wrong.
2. A code-generator like quicktype or convertifydata's tool
Paste JSON, get types out. Our JSON → TypeScript tooldoes this in the browser. It works well for one-off "I have a sample response, I want stubs" situations.
The trade-off: generated types reflect one sample. If your sample happens to have a field as null, the generator might type it as null when the real type is string | null. Always feed a representative sample, ideally several merged together.
3. JSON Schema → TypeScript
If you have a JSON Schema (or can generate one), tools like json-schema-to-typescript produce types that match the spec, not just one sample. This is the right answer when the schema is the source of truth — OpenAPI specs, JSON Schema files in a repo, config schemas.
4. OpenAPI → TypeScript
For HTTP APIs documented with OpenAPI, generate the entire client. Tools like openapi-typescriptproduce a typed surface for every endpoint, request, and response. This is the closest to "types are a side effect of the API definition" that you can get without gRPC.
5. Runtime validators (Zod, io-ts, Valibot)
The most defensive option. Instead of static interfaces, define a runtime schema and infer the type. At runtime you can actually check that incoming data matches; at compile time you get the same type as in option 1.
import { z } from "zod";
const User = z.object({
id: z.string(),
email: z.string().email(),
age: z.number().int().optional(),
});
type User = z.infer<typeof User>;
// In code that handles untrusted input:
const user = User.parse(json); // throws on bad data
// 'user' is now typed *and* validatedThis is the right answer at every trust boundary: API responses, form inputs, message queues, anything you did not produce yourself. The cost is the bundle size of the validator and the discipline of writing schemas instead of just interfaces. The convertifydata JSON → Zod tool generates these from sample JSON to save you the typing.
Which one to use
| Situation | Use |
|---|---|
| Tiny, stable payload | Hand-write |
| One-off "I have a sample" | JSON → TypeScript generator |
| You have / want a JSON Schema | json-schema-to-typescript |
| HTTP API with OpenAPI spec | openapi-typescript |
| Untrusted input at runtime | Zod / io-ts / Valibot |
The mistake to avoid is picking option 1 or 2 for an input you do not control. Hand-written types and generator output both lie the moment upstream changes, and TypeScript will happily keep compiling on top of a lie. For anything coming over the network, get a runtime check in the loop.