Stop Hand-Writing Your Types: The Engineering Case for JSON-to-Type Codegen

#1JSON-to-type codegen: why hand-written boundary types drift
What we tested: We processed API response payloads ranging from 1 KB to 50 MB through each JSON tool on this site. Formatting, validation, and conversion times were measured in Chrome 128 with DevTools performance panel. All processing runs locally in the browser, no server round-trips.
Copying a JSON payload into a TypeScript interface, a Pydantic model, or a Go struct by hand is basically data entry.
Generated types are useful at the boundary because they remove transcription errors and make API drift obvious the moment the shape changes.
#2The Hidden Cost of Hand-Transcription
Picture the everyday task: an endpoint returns a 40-field JSON object with three levels of nesting, and you need a type for it. So you start typing. id: string, createdAt: string, items: {.... Fifteen minutes later you have an interface that's approximately right.
"Approximately" is the whole problem. Every hand-written type carries a set of quiet defects:
- Transcription typos.
recieved,discountAmmount,is_verifed. The compiler can't catch a field name that's wrong in both the type and the code that reads it, it just silently readsundefinedforever. - Optional vs. required guesses. JSON doesn't announce which fields are nullable. By eye you assume; in production a
nullyou typed as required blows up three weeks later. - Wrong primitive widths. Is that ID a number or a string? Is the timestamp an ISO string or a Unix integer? A single sample answers this; guessing doesn't.
- Nested-shape fatigue. By the third level of nesting, attention flags and you approximate. Approximations in type definitions are lies the compiler will faithfully enforce.
None of these are exotic. They're the median outcome of asking a human to be a JSON parser for fifteen minutes.
#2What Generation Buys You
Feeding a representative JSON sample to a generator flips every one of those defects into a guarantee:
- Zero transcription error. The field names come from the data, byte for byte.
discountAmmountcan't appear unless the API actually spells it that way, in which case your type now matches reality, which is exactly what you want. - Structure inferred, not guessed. Nesting, arrays, and primitive types are read off the actual payload. A generator sees
"count": 0and writesnumber; it sees"tags": []and writes an array type. - Speed. Fifteen minutes becomes fifteen seconds. On a large payload the time saving is real, but the error saving is the point.
- A single source of truth. When the API changes, you paste the new sample and regenerate. The diff shows you exactly what moved. Hand-maintained types have no such feedback loop, they drift until something breaks.
This is the same principle behind compiling rather than interpreting-by-hand: let a machine do the mechanical, error-prone transformation so humans spend attention on the parts that need judgment.
#3Real example: quicktype in a CI step
Here's a minimal setup we run in our own projects. Given this sample API response (order.json):
{
"id": "ord_9f8a3b",
"createdAt": "2026-08-20T14:30:00Z",
"items": [
{ "sku": "WIDGET-42", "qty": 3, "unitPrice": 12.99 }
],
"discountCode": null,
"totalCents": 3897
}Running npx quicktype -s json order.json -o order-types.ts --just-types produces:
// Auto-generated, do not edit by hand
export interface Order {
id: string;
createdAt: string;
items: Item[];
discountCode: null;
totalCents: number;
}
export interface Item {
sku: string;
qty: number;
unitPrice: number;
}Notice discountCode: null, the generator saw null in the sample and typed it honestly. A hand-written type would likely have guessed string and been wrong. When the API starts returning actual discount codes, regenerating produces a diff that shows you exactly what changed.
#2Types Are Not Enough: Generate Validators Too
Here's the sharper version of the argument. A TypeScript interface is a compile-time fiction, it vanishes at runtime. If your API actually returns a null where your interface promised a string, TypeScript does nothing; you get a runtime crash far from the source, with a type system that swore everything was fine.
The fix is to generate a runtime validator, not just a type. A Zod schema generated from your JSON sample gives you both a static type (via z.infer) and a runtime parse() that actually checks the data at the boundary, so a shape mismatch fails loudly, at the edge, with a precise error, instead of silently poisoning your program. The same holds for Pydantic in Python: the model is the validator. (We've made this case at length in Stop Trusting LLM JSON, the argument generalizes to any untrusted JSON source, not just LLMs.)
Rule of thumb: types for shape, validators for trust. At any boundary where data enters your program from outside, an API, a file, a queue, a user, generate a validator. Pure internal data can stay with plain types.
#2Where the Boundary Should Sit
Codegen is not a mandate to generate everything. Overusing it produces sprawling, anemic types that mirror every wart of an external API and leak that shape through your whole codebase. The discipline is knowing which types to generate and which to author by hand.
| Generate it | Hand-author it |
|---|---|
| DTOs for third-party API responses | Your core domain model |
| Types for a JSON config or fixture | Types with invariants the JSON can't express |
| One-off deserialization at a boundary | Types that encode business rules |
| Wire formats you don't control | The vocabulary your team reasons in |
The pattern that scales: generate the boundary type, then map it to a hand-authored domain type at the edge. The generated ApiUserDTO is disposable and regenerated whenever the API moves; your User domain type is stable, expressive, and owned by you. The mapping function between them is where you make deliberate decisions, renaming discountAmmount to discountAmount, narrowing a stringly-typed status into a union, dropping fields you don't need.
#2A Fast Workflow
- Grab a representative JSON response, ideally one with the optional fields populated and the arrays non-empty, so the generator infers the fullest shape. Empty arrays and absent optionals are where generators guess.
- Paste it into the generator for your language: TypeScript, Zod, Pydantic, Go, or Rust.
- Review the output. Fix anything the single sample couldn't know, mark genuinely-optional fields, widen an enum, correct a field the sample happened to show as
null. - For boundary data, prefer the validator output (Zod/Pydantic) and call
parse()on the way in. - Commit the generated file with a comment noting it's generated and from where, so the next person regenerates instead of hand-patching.
Because generators infer from one sample, their one real weakness is fields your sample doesn't exercise. Feed the richest example you have, and treat the output as a strong first draft you refine, not gospel.
#2Frequently Asked Questions
#3Isn't generated code lower quality than hand-written types?
For the transcription task, generated types are higher quality, not lower, they can't misspell a field or miscopy a nested shape, which are the most common defects in hand-written types. Where hand-authoring wins is judgment: encoding invariants the JSON can't express, naming things in your domain's vocabulary, and narrowing loose wire types into precise ones. The mature approach uses both, generate the disposable boundary DTO from the payload, then hand-author the stable domain type and map between them at the edge. You get the machine's accuracy on mechanical work and human judgment where it actually adds value.
#3Should I generate a type or a runtime validator from my JSON?
Generate a runtime validator (Zod in TypeScript, Pydantic in Python) for any data that enters your program from outside, an API, a file, a message queue, user input. A plain TypeScript interface is erased at compile time and does nothing if the real data has the wrong shape, so a mismatch becomes a mysterious runtime crash far from the source. A validator checks the data at the boundary and fails loudly with a precise error exactly where the bad data entered. For purely internal data you control end to end, a plain generated type is sufficient. The rule: types for shape, validators for trust.
#3How do I keep generated types in sync when the API changes?
Treat the generated file as regenerated output, not hand-maintained source. When the API changes, paste a fresh representative response into the generator and regenerate the whole file; the version-control diff then shows you exactly which fields were added, removed, or changed. Add a header comment recording that the file is generated and where the sample came from, so teammates regenerate instead of hand-patching, hand-patching is what reintroduces the drift you adopted codegen to avoid. Keeping the boundary DTO separate from your hand-authored domain type also localizes the churn: only the disposable DTO changes, and your mapping function tells you at compile time what broke.
#3What's the catch with generating types from a single JSON sample?
The generator only knows what your sample shows it. If a field is optional and your sample omits it, the type won't include it; if a nullable field happens to be non-null in your sample, it'll be typed as required; an empty array gives the generator nothing to infer an element type from. The mitigation is to feed the richest representative payload you have, optional fields populated, arrays non-empty, and then review the output for exactly these cases, marking true optionals and correcting any field the sample under-specified. Think of generation as producing a strong first draft that removes the tedious 90%, leaving you to refine the 10% that requires knowledge the sample didn't carry.
#3Which languages can I generate types for from JSON?
Effectively all the mainstream ones, and this site covers the common targets directly: TypeScript interfaces, Zod schemas (runtime-validated TypeScript), Python Pydantic models, Go structs, and Rust structs with serde annotations. The workflow is identical across all of them, paste a representative JSON payload, get idiomatic type definitions for that language, review for optional/nullable edge cases, and use the validator variants (Zod, Pydantic) at trust boundaries. Choosing the target is just a matter of your stack; the accuracy-and-speed argument for generating rather than hand-writing applies equally to every one.
Hand-transcribing a JSON payload into a type definition is exactly the kind of mechanical, error-prone work computers exist to do. Let the generator handle the transcription flawlessly, spend your attention on the domain types and boundary mappings that need judgment, and reach for validators, not just types, wherever untrusted data enters.
Generate idiomatic types from any JSON sample now: TypeScript, Zod, Pydantic, Go, and Rust.
Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.
#2Sources / Further reading
- JSON Schema - TypeScript code generation
- quicktype - Generate types from JSON
- OpenAPI Generator - Auto-generate client SDKs
#2Try These Tools
Quick Summary
>- Hand-writing type definitions from a JSON payload is slow, error-prone, and drifts out of sync the moment the API changes. This is the case for generating your types — TypeScript, Zod, Pydantic, Go structs, Rust — directly from a representative JSON sample, why it eliminates a whole class of runtime bugs, and where the boundary between generated and hand-authored types should sit.
Key Takeaways
- Hand-writing TypeScript types from JSON responses is error-prone — real API responses include optional fields, null values, and edge cases a single sample does not reveal.
- Code generation tools (quicktype, openapi-generator, json-schema-to-ts) produce types from JSON Schema, OpenAPI specs, or sample JSON automatically.
- Generated types should be reviewed and refined — auto-generated types capture structure but not business semantics.
When to use it
- Generating TypeScript interfaces from an OpenAPI 3.1 specification for type-safe API clients.
- Converting a JSON API response sample into TypeScript types using quicktype.
- Generating Rust structs from JSON schemas for a backend service.
Common Mistakes
- Generating types from a single API response and assuming all fields are required — real traffic includes optional and null fields.
- Not validating the OpenAPI spec before generating types — garbage in, garbage out.
- Using generated types without adding JSDoc or comments — auto-generated names may not match your domain vocabulary.
Stop Hand-Writing Your Types: The Engineering Case for JSON-to-Type Codegen, Frequently Asked
What is the best tool for generating TypeScript types from JSON?
For OpenAPI specs: openapi-typescript or openapi-generator. For raw JSON samples: quicktype.io. For JSON Schema: json-schema-to-ts (compile-time) or json-schema-to-typescript (runtime).
Should I trust auto-generated types completely?
No. Generated types capture structural types from the input, but they cannot infer business semantics. Always review generated types and add domain-specific documentation.
Tools Mentioned in This Article
JSON to TypeScript
Convert JSON objects into TypeScript interfaces instantly.
JSON Schema Generator
Auto-generate a JSON Schema from any JSON object with format detection.
JSON to Drizzle Schema
Generate Drizzle ORM schema definitions from JSON objects.
JSON to Prisma Schema
Generate Prisma model definitions from JSON objects.
Tools, tactics, and toughened-up tips, once a week
New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.
Found an error or have feedback?
We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.