JSON to TypeScript: workflows that scale beyond a single sample
A one-shot JSON โ interface tool is great for demos and dead for production. Here are the patterns real teams use to keep types in sync with real APIs.
The one-shot generator gets you 80% of the way
Paste a JSON sample, get an interface. That works for:
- Sketching types from a sample API response you've received
- Turning a config file into a typed config
- Bootstrapping types for a new integration
It stops working when the API has multiple response shapes, optional fields that aren't in your sample, or nested objects where the same shape appears in different places.
The three "same JSON, different types" problems
1. Missing optionals. Your sample has { id: 1, name: "Ada" }; the API sometimes returns { id: 2, name: "Bob", email: "b@b" }. A one-shot generator produces the narrower type and TypeScript now lies about the API.
Fix: generate from multiple samples and mark differing fields optional. Or hand-edit the generated interface to add email?: string - often the fastest path.
2. Sum types (discriminated unions). Your API returns { type: "success", data: {...} } on happy path and { type: "error", message: "..." } on failure. A one-shot generator sees them as one merged type with all fields optional. That's the wrong shape.
Fix: generate each variant separately, then hand-combine into a discriminated union:
type Response =
| { type: "success"; data: Data }
| { type: "error"; message: string };
3. Recursive types. { id: 1, children: [{ id: 2, children: [...] }] }. Generators emit inline nested types up to a depth, then give up. Rewrite as a named recursive type:
interface Tree { id: number; children: Tree[]; }
When to use runtime validation instead
TypeScript types are erased at compile time. At runtime the API can return literally anything. If the types are load-bearing (feed into a database write, expose to users, drive UI branching), you need runtime validation.
Two popular patterns:
Zod. Write the schema once, infer the type, validate at runtime:
import { z } from "zod";
const User = z.object({
id: z.number(),
name: z.string(),
email: z.string().email().optional(),
});
type User = z.infer<typeof User>;
const result = User.safeParse(apiResponse);
if (!result.success) throw new Error(result.error.message);
JSON Schema + json-schema-to-typescript. OpenAPI already gives you JSON schemas for every endpoint; generate TS types from those. The pipeline is more setup but keeps a single source of truth.
When to skip TypeScript types entirely
If the JSON is truly unstructured (a JSONB blob, a webhook body you don't own), typing it is theatre. Use unknown and validate the fields you actually access:
function getName(response: unknown): string {
if (typeof response !== "object" || response === null) return "unknown";
const r = response as Record<string, unknown>;
return typeof r.name === "string" ? r.name : "unknown";
}
The workflow for adding a new API endpoint
The pattern that works on real teams:
- Capture a real response. Not a mock, not the docs - hit the actual endpoint.
- Format it so you can see the shape at a glance.
- Generate an initial interface with JSON โ TypeScript.
- Hand-edit to mark optionals, add unions, name nested types semantically.
- Cross-check with a second sample - call the endpoint a few more times, with different inputs, and diff the responses against the first to find fields that vary.
- Add runtime validation with Zod or a JSON schema, if the response drives anything critical.
The generator gives you a starting shape in ten seconds. Steps 4-6 give you types you can trust.
Interface or type alias?
interface User { id: number; name: string; }
type User = { id: number; name: string; };
At runtime - identical. In practice:
interfacewins for object shapes that other code willextendsfrom. Better error messages in the IDE.typewins for unions, intersections, mapped types, and literal types.
Both work. Pick one, use consistently in a codebase.
Keeping types in sync with a moving API
If you own both sides, generate types from the schema, not the samples. If you don't own the API:
- Snapshot the response weekly in CI and diff against last week's. If the shape changes, alert.
- Validate every response at runtime so a silent shape change fails loud in staging, not in production.
Related tools:
- JSON Formatter - for the "eyeball the shape" step.
- JSON Diff - for the "compare two responses" step.
Tools mentioned in this post
Related reading
HTTP status codes that actually matter for API developers in 2026
You know 200 and 500. But the difference between 401 vs 403, 400 vs 422, and 502 vs 503 decides whether your API is professional or amateur. Here's the short list that matters.
JSON Schema for LLM structured output: the developer's shortcut
OpenAI's structured output and Anthropic's tool use both want JSON Schema. Here's the draft that works everywhere, the fields models care about, and how to write one from an example in 10 seconds.
YAML pitfalls: the Norway problem, indentation, and anchors
YAML looks like the friendly config format until it silently converts 'NO' to false. Here are the traps in YAML 1.1, why 1.2 fixes some of them, and when to switch to JSON.
Why your JSON won't parse (and how to fix it in 30 seconds)
Trailing commas, unquoted keys, single quotes, comments - the five errors that break 90% of hand-written JSON, and how to catch them without a build step.
Shan builds 712 Tools. He holds a Master's degree in Mechanical Engineering and now works as a Software Engineer, shipping browser-based developer utilities out of Ontario, Canada. Learn more ยท 712studiogames@gmail.com