{ }jsonkitOpen

JSON to TypeScript: how to generate types from JSON

9 min read · updated 2026-07-12

Converting JSON to TypeScript gives your API responses real types. Every fetch call returns any or unknown until you tell TypeScript what shape to expect. Writing those interfaces by hand for a deeply nested response is slow and easy to get wrong, so many teams generate them from a JSON sample.

This guide shows how the conversion works. Nested objects become nested interfaces, arrays map to T[], optional properties are detected across array items, and mixed value types become unions. It then compares an online generator, the quicktype CLI and manual typing. It ends with the one thing generated types cannot do: validate data at runtime.

Why typing API responses matters

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

Typing the response reverses that. Once the shape is declared, the compiler catches user.emial at build time, autocomplete shows every field, and renaming a property updates the whole codebase safely. For a nested payload, autocomplete on data.results[0].profile.avatarUrl beats guessing at it and debugging later.

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

Writing these interfaces by hand for a real API, with pagination wrappers, nested relations and arrays of objects, is slow and easy to get wrong. Generating them from a real 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 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 pulls it out into its own named interface instead of inlining an anonymous type. The output stays readable, and you can 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"] }
  ]
}

The 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 what you would change it to by hand 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 looks at the array's elements to infer T. That is also where optional properties are detected, as covered next.

Optional properties, nulls and unions

Real JSON is rarely uniform. A field can be present in one object and missing in another, or hold a string here and a number there. How a generator handles this decides whether you get a usable type or one you have to rewrite.

Optional properties detected across array items

A single object cannot tell you whether a field is optional, because every key it has is present. You only see optional fields across several samples. When a generator sees an array of objects, it collects every key 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[];
}

That is why a single-object sample often produces types with no optional fields. There is nothing to compare against. An array with realistic variation gives the generator the evidence it needs.

Null values and unions for mixed types

A JSON null on its own cannot be typed precisely, because it says nothing about what the field holds when it is not null. Generators handle it in one of two ways. If null is all they see, they emit null or any. If the same field is sometimes null and sometimes a value, they produce a union like string | null.

The same applies when a field holds different primitive types across samples. If id appears as both a number and a string, a good generator emits a union instead of quietly 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;
}

JSON to TS: online tool vs quicktype vs manual

There are three common ways to get from JSON to TypeScript. They differ in speed, control and how much you can automate.

JSON to TypeScriptPaste a JSON sample and get TypeScript interfaces, with nested objects as named types, arrays as T[] and optional fields detected. It runs in your browser, so nothing is uploaded.→

The online generator

Best for a quick one-off conversion during development. Paste a response and copy the interfaces. A browser-based tool like JSON to TypeScript keeps the data on your machine, which matters when the payload is sensitive. The downside is that it is not part of your build, so you re-run it by hand when the API changes.

quicktype (CLI or library)

quicktype is the strongest option for automation. It reads JSON or JSON Schema and emits types for TypeScript and many other languages. You can script it 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 full control over naming, optional fields and unions. No generator names your types or splits shared shapes as well as you can. It is the right choice for small, stable payloads, or as a cleanup pass over generated output. For a large nested response it is 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

Most teams combine them. They generate a first pass with a tool, then fix names, tighten unions and mark the optional fields the samples did not reveal.

Types are erased: why you still need runtime validation

This catches many newcomers. TypeScript types exist only at compile time. They are removed completely when your code compiles to JavaScript. At runtime there is no User interface and no check. A generated type is a promise you make to the compiler, not a guarantee about the data.

So this compiles cleanly and is wrong as soon as the API returns something unexpected:

The cast checks nothing 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. It checks the actual data and derives the static type from the same schema, so you have 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, so no separate interface can 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());
}

To formalize the contract as a spec, generate a JSON Schema from your sample and feed it into codegen. quicktype and several validators accept JSON Schema as input, so one definition serves both docs and validation.

A practical workflow

Put together, a reliable process looks like this:

  1. Capture a real, representative response. Several are better, including edge cases with missing or null fields, so optional fields and unions show up.
  2. Generate a first set of interfaces with JSON to TypeScript or quicktype.
  3. Review the output. Rename generic types (Root, Item) to domain names, check optional fields, and look into any surprising unions.
  4. At every trust boundary, replace the bare cast with a runtime validator (Zod or Valibot) and derive the type from the schema, so the type and the check never drift apart.
  5. Regenerate whenever the API contract changes. Treat it as codegen, not a one-time task.

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

Frequently asked questions

How do I convert JSON to TypeScript interfaces?

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 names and confirm optional fields.

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

For plain object shapes they are nearly interchangeable. interface supports declaration merging and is the usual choice for object contracts. type is required for unions, intersections and mapped types. Most generators emit interface for objects and use type when a union is needed, such as type Id = number | string.

How are optional properties detected from a JSON sample?

Optional fields are inferred across the elements of an array, not from a single object. The generator collects the keys of every element and marks any key missing from at least one element with ?. A single-object sample therefore usually has no optional fields. Use 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 do not exist at runtime, so a generated interface never checks the actual data. The compiler trusts a cast like data as User, but nothing verifies it. Use a runtime validator such as Zod at trust boundaries, and derive the static type from the same schema with z.infer.

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 on your machine. Use quicktype when you want repeatable codegen in your build, or need other target languages or JSON Schema input. Many teams generate a first pass with either and then adjust it by hand.

How does the generator handle null and mixed types?

A lone null cannot be typed precisely, so generators emit null or any. If the field is sometimes null and sometimes a value, they produce a union like string | null. When the same field holds different primitive types across samples, a good generator emits a union such as number | string instead of quietly picking one.