{ }jsonkitOpen the tool

How to Generate TypeScript Types from JSON

9 min read · updated 2026-07-12

Every fetch call returns any or unknown until you tell TypeScript what shape to expect. Writing those interfaces by hand for a deeply nested API response is tedious and error-prone, which is why generating TypeScript types directly from a JSON sample has become a standard step in front-end and full-stack workflows.

This guide walks through how the conversion actually works: how nested objects become nested interfaces, how arrays map to T[], how optional properties are detected across array items, and how mixed value types collapse into unions. It then compares an online generator, the quicktype CLI, and manual typing, and closes with the one thing generated types can't do: validate data at runtime.

Why Typing API Responses Matters

An untyped API response is a silent liability. When await res.json() returns any, every property access downstream compiles even when the field is misspelled, missing, or the wrong type. The mistake surfaces at runtime as Cannot read properties of undefined — usually in production, usually far from the fetch that caused it.

Typing the response flips that around. Once the shape is declared, the compiler catches user.emial at build time, autocomplete surfaces every field, and refactors that rename a property ripple through the codebase safely. For a nested payload, the difference between guessing at data.results[0].profile.avatarUrl and having it autocompleted is the difference between a quick task and a debugging session.

  • Compile-time safety: typos and shape mismatches fail the build, not the user's browser.
  • Editor autocomplete and inline docs for every field of the response.
  • Safe refactoring: renaming a field updates every call site through the type system.
  • Self-documenting code: the interface is the contract, readable without opening the API docs.

The catch is that hand-writing these interfaces for a real API — pagination wrappers, nested relations, arrays of objects — is slow and easy to get wrong. Generating them from an actual sample response removes the guesswork.

From a JSON Sample to TypeScript Interfaces

The core mapping is mechanical. A JSON object becomes an interface, each key becomes a property, and each value's runtime type maps to a TypeScript type: strings to string, numbers to number, booleans to boolean. The interesting cases are the recursive ones — nested objects and arrays.

Nested objects become named interfaces

When a value is itself an object, a good generator extracts it into its own named interface rather than inlining an anonymous type. That keeps the output readable and lets you reference the sub-type elsewhere. Take this sample response:

A sample user payload with a nested object and an array of objectsjson
{
  "id": 42,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "isActive": true,
  "roles": ["admin", "editor"],
  "profile": {
    "bio": null,
    "avatarUrl": "https://example.com/a.png"
  },
  "posts": [
    { "id": 1, "title": "Hello", "tags": ["intro"] }
  ]
}

A structural conversion produces three interfaces — the root object plus one for each nested object shape. Note bio: the sample only shows null, so a generator can infer null or any at best; the string | null below is the type you would hand-tune it to once you know the field is a nullable string.

Generated interfaces: nested objects and arrays resolved to named typestypescript
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  roles: string[];
  profile: Profile;
  posts: Post[];
}

interface Profile {
  bio: string | null;
  avatarUrl: string;
}

interface Post {
  id: number;
  title: string;
  tags: string[];
}

Arrays map to T[]

An array of strings becomes string[] (roles above), and an array of objects becomes an array of the extracted element interface (posts: Post[]). The generator inspects the array's elements to infer T — which is where optional-property detection comes in, covered next.

Optional Properties, Nulls, and Unions

Real JSON is rarely uniform. The same field can be present in one object and absent in another, or hold a string here and a number there. How a generator handles these variations is what separates a usable type from one you have to rewrite by hand.

Optional properties detected across array items

A single object can't tell you whether a field is optional — every key it has is, by definition, present. Optionality only becomes visible across multiple samples. When a generator sees an array of objects, it takes the union of all keys and marks any key missing from at least one element with ?:

discount is present on the first item but missing on the secondjson
{
  "items": [
    { "id": 1, "name": "Keyboard", "discount": 0.1 },
    { "id": 2, "name": "Mouse" }
  ]
}
discount becomes optional because it is absent from at least one elementtypescript
interface Item {
  id: number;
  name: string;
  discount?: number;
}

interface Root {
  items: Item[];
}

This is why a single-object sample often produces types with no optional fields at all — there's nothing to compare against. Feeding in an array with realistic variation gives the generator the evidence it needs.

Null values and unions for mixed types

A JSON null can't be typed precisely on its own — null alone tells you nothing about what the field holds when it isn't null. Generators handle it in one of two ways: emit null (or any) if that's all they see, or, given a sample where the same field is sometimes null and sometimes a value, produce a union like string | null.

The same logic applies when a field holds different primitive types across samples. If id appears as both a number and a string, a good generator collapses that into a union rather than silently picking one:

Mixed value types across samples produce a uniontypescript
interface DataRecord {
  // "id": 1 in one sample, "id": "a1" in another
  id: number | string;
  // present-and-null in some samples, a string in others
  label: string | null;
}

Generating Types: Online Tool vs quicktype vs Manual

There are three common ways to get from JSON to TypeScript. They trade off speed, control, and how much of the pipeline you're willing to automate.

→tsJSON to TypeScriptPaste a JSON sample and get clean TypeScript interfaces instantly — nested objects extracted into named types, arrays resolved to T[], and optionals inferred. It runs entirely in your browser, so response payloads never leave your machine.

The online generator

Best for a fast, one-off conversion during development: paste a response, copy the interfaces, done. A browser-based tool like JSON to TypeScript keeps the data client-side, which matters when the payload contains anything sensitive. The trade-off is that it isn't wired into your build, so you re-run it manually whenever the API changes.

quicktype (CLI / library)

quicktype is the most capable option for automation. It reads JSON (or JSON Schema) and emits types for TypeScript and many other languages, and it can be scripted into a codegen step so types regenerate from sample fixtures on every build.

Generate a TypeScript definitions file from a JSON sample with quicktypebash
npx quicktype user.json -o user.ts --lang typescript --just-types

Manual typing

Writing interfaces by hand gives total control over naming, optionality, and unions — no generator will name your types or split shared shapes as well as you can. It's the right call for small, stable payloads, or as a cleanup pass over generated output. For a large nested response it's simply too slow and too easy to miss a field.

ApproachBest forSpeedAutomatableData stays local
Online generatorQuick one-off conversions during devInstantNoYes (client-side tools)
quicktypeRepeatable codegen in a build pipelineFastYesYes (runs locally)
Manual typingSmall, stable payloads; final cleanupSlowN/AYes

In practice most teams combine them: generate a first pass with a tool, then hand-tune names, tighten unions, and mark the optionals the samples didn't reveal.

Types Are Erased: Why You Still Need Runtime Validation

Here's the gap that trips up newcomers. TypeScript types exist only at compile time — they're stripped out entirely when your code is transpiled to JavaScript. At runtime there is no User interface, no check, nothing. A generated type is a promise you make to the compiler, not a guarantee about the data.

So this compiles cleanly and lies to you the moment the API returns something unexpected:

The cast is a lie: no check happens at runtimetypescript
async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  // TypeScript trusts you here; the runtime does not verify anything
  return (await res.json()) as User;
}

If the server changes a field, returns null where you expected a string, or the request hits an error page, data.profile.avatarUrl still type-checks and still crashes. The fix is a runtime validator such as Zod, which checks the actual data and derives the static type from the same schema — one source of truth:

Zod validates at runtime and infers the TypeScript type from the same schematypescript
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  isActive: z.boolean(),
  roles: z.array(z.string()),
  profile: z.object({
    bio: z.string().nullable(),
    avatarUrl: z.string().url(),
  }),
});

// The static type is derived from the schema — no separate interface to drift
type User = z.infer<typeof UserSchema>;

async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  // Throws a descriptive error if the data doesn't match
  return UserSchema.parse(await res.json());
}

If you'd rather formalize the contract as a spec, generate a JSON Schema from your sample and feed that into codegen — quicktype and several validators accept JSON Schema as input, giving you one canonical definition for both docs and validation.

A Practical Workflow

Putting it together, a reliable end-to-end process looks like this:

  1. Capture a real, representative response — ideally several, including edge cases with missing or null fields so optionals and unions are detectable.
  2. Generate a first-pass set of interfaces with JSON to TypeScript or quicktype.
  3. Review the output: rename generic types (Root, Item) to domain names, verify optionals, and inspect any surprising unions.
  4. At every trust boundary, replace the bare cast with a runtime validator (Zod/Valibot) and derive the type from the schema so type and check never drift apart.
  5. Regenerate whenever the API contract changes — treat it as codegen, not a one-time task.

The generated types make development fast and safe; the runtime schema makes production safe. You need both, and keeping the type derived from the schema means you only maintain one definition.

Frequently asked questions

How do I generate TypeScript interfaces from JSON?

Paste a JSON sample into a generator like JSON to TypeScript or run quicktype input.json --lang typescript --just-types. The tool maps objects to interfaces, arrays to T[], and nested objects to their own named interfaces. Review the result to fix naming and confirm optional fields.

What's the difference between an interface and a type alias for JSON shapes?

For plain object shapes they're nearly interchangeable. interface supports declaration merging and is conventional for object contracts; type is required for unions, intersections, and mapped types. Most generators emit interface for objects and fall back to type when a union is needed, such as type Id = number | string.

How are optional properties detected from a JSON sample?

Optionality is inferred across the elements of an array, not from a single object. The generator takes the union of all keys across every element and marks any key missing from at least one element with ?. A single-object sample therefore usually produces no optional fields — feed in an array with realistic variation to detect them.

Do TypeScript types generated from JSON validate data at runtime?

No. TypeScript types are erased during compilation and don't exist at runtime, so a generated interface never checks the actual data. A cast like data as User is trusted by the compiler but verifies nothing. Use a runtime validator such as Zod at trust boundaries and derive the static type with z.infer from the same schema.

Should I use the online tool or quicktype?

Use an online tool for quick, one-off conversions during development — a client-side one keeps sensitive payloads local. Use quicktype when you want repeatable codegen wired into your build, or need to target multiple languages or JSON Schema input. Many teams generate a first pass with either, then hand-tune the result.

How does the generator handle null and mixed types?

A lone null can't be typed precisely, so generators emit null/any or, given samples where the field is sometimes null and sometimes a value, a union like string | null. When the same field holds different primitive types across samples, a good generator collapses them into a union such as number | string rather than silently picking one.