Skip to main content
AllDevToolsHub
2026-07-01
Last reviewed: Aug 2026
DX
Est Read: 09_MIN

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

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

#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 reads undefined forever.
  • Optional vs. required guesses. JSON doesn't announce which fields are nullable. By eye you assume; in production a null you 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:

  1. Zero transcription error. The field names come from the data, byte for byte. discountAmmount can't appear unless the API actually spells it that way, in which case your type now matches reality, which is exactly what you want.
  2. Structure inferred, not guessed. Nesting, arrays, and primitive types are read off the actual payload. A generator sees "count": 0 and writes number; it sees "tags": [] and writes an array type.
  3. Speed. Fifteen minutes becomes fifteen seconds. On a large payload the time saving is real, but the error saving is the point.
  4. 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):

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:

typescript
// 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 itHand-author it
DTOs for third-party API responsesYour core domain model
Types for a JSON config or fixtureTypes with invariants the JSON can't express
One-off deserialization at a boundaryTypes that encode business rules
Wire formats you don't controlThe 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

  1. 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.
  2. Paste it into the generator for your language: TypeScript, Zod, Pydantic, Go, or Rust.
  3. 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.
  4. For boundary data, prefer the validator output (Zod/Pydantic) and call parse() on the way in.
  5. 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

#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

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.
Use Cases

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.
Watch out

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.
FAQ

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.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-07-01Last reviewed 2026-08-23

Tools Mentioned in This Article

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.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.