Skip to content
Advanced

How to Convert JSON to TypeScript Types (and Avoid False Confidence)

·5 min read
How to Convert JSON to TypeScript Types (and Avoid False Confidence) - Advanced guide from JSON Prettifier

Quick answer: Map each JSON value to its TypeScript counterpart (objects to interfaces, arrays to T[], null to a | null union) and mark a key optional with ? only when the API can omit it. Generators such as quicktype produce a first draft from a sample, but TypeScript types describe the shape your code expects; they do not validate JSON received at runtime. For data that crosses a network boundary, pair the type with a runtime check such as a zod schema or a type guard.

You receive a JSON sample from an API and need TypeScript types for your frontend. The first version looks straightforward: strings are strings, numbers are numbers, and arrays use brackets. Then the API sends null for a field you assumed was always present.

Inspect the JSON structure in the JSON Viewer. This helps with reading the payload; schema validation still requires a separate schema validator.

Start with a real JSON response

Suppose your API returns:

{
  "id": 1042,
  "name": "Maya",
  "active": true,
  "roles": ["editor", "reviewer"],
  "profile": {
    "city": "Ahmedabad",
    "website": null
  }
}

A reasonable TypeScript representation is:

interface User {
  id: number;
  name: string;
  active: boolean;
  roles: string[];
  profile: {
    city: string;
    website: string | null;
  };
}

Objects become object-shaped types. Arrays become T[]. JSON null is represented by null, usually in a union with the other expected value type. Note that JSON has no date type: a timestamp arrives as a string, and converting it to a Date is your code's job, not the type's (see the JSON date format guide).

Don't confuse null with an optional key

These two responses mean different things:

{"name":"Maya","website":null}
{"name":"Maya"}

In the first, website exists and has a null value. In the second, the key is missing. The JSON null vs undefined guide explains why serializers treat the two cases differently.

TypeScript can express that distinction:

interface Profile {
  name: string;
  website?: string | null;
}

The ? means the property may be absent; | null means it may explicitly contain null. Use both only if your API really permits both cases. Don't add optional markers to every property just to silence compiler errors—that hides contract problems.

Can I derive types automatically?

Yes, and for a large nested response it is the sensible first pass. Tools fall into two groups:

  • From a sample: quicktype infers interfaces from example JSON. Paste the response, pick TypeScript, and review the output.
  • From a contract: json-schema-to-typescript generates interfaces from a JSON Schema, and openapi-typescript does the same from an OpenAPI description. If the API publishes either, generate from it rather than from a sample, because the contract records optional fields and enums that a single example cannot.

A single sample cannot tell you whether a field is sometimes absent, whether a status can take three different string values, or whether an ID could exceed JavaScript's safe integer range (the JSON numbers and precision guide covers that last case). Review generated types against API documentation and multiple representative responses. Examples are evidence, not a complete schema.

Better modeling with a union

If the server documents only three possible statuses, represent that rule explicitly:

type OrderStatus = 'pending' | 'paid' | 'cancelled';

interface Order {
  id: number;
  status: OrderStatus;
  totalMinorUnits: number;
}

This helps catch mistakes such as assigning 'complete' in your frontend source code. It does not prevent a faulty API from sending 'complete' at runtime.

Validate external data before trusting it

This pattern looks safe but is not a runtime check:

const response = await fetch('/api/user/1042');
const user = (await response.json()) as User;

as User tells TypeScript to trust you. It doesn't inspect the response or throw when a field is wrong. (The fetch JSON guide covers the status and content-type checks that should wrap this call.)

For a small example, a hand-written type guard can check the key fields:

function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) return false;
  const item = value as Record<string, unknown>;
  return (
    typeof item.id === 'number' &&
    typeof item.name === 'string' &&
    typeof item.active === 'boolean' &&
    Array.isArray(item.roles) &&
    item.roles.every(role => typeof role === 'string') &&
    typeof item.profile === 'object' &&
    item.profile !== null &&
    typeof (item.profile as Record<string, unknown>).city === 'string' &&
    (
      typeof (item.profile as Record<string, unknown>).website === 'string' ||
      (item.profile as Record<string, unknown>).website === null
    )
  );
}

Guards like this get tedious as shapes grow, which is where a runtime validation library earns its place. With zod, the schema is the single source of truth and the TypeScript type is derived from it:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  active: z.boolean(),
  roles: z.array(z.string()),
  profile: z.object({
    city: z.string(),
    website: z.string().nullable(),
  }),
});

type User = z.infer<typeof UserSchema>;

const user = UserSchema.parse(await response.json()); // throws on mismatch

If your team already maintains a JSON Schema for the API, a JSON Schema validator can play the same role and the schema can be shared with backend tooling. Whichever you choose, define how your app handles null, extra keys and unexpected numeric values.

Inspect the JSON first

Before creating types, format the original JSON so nested structures are visible. If it fails syntax validation, fix that issue first. Then decide which fields belong in an interface, which should be optional, and which require runtime checks.

Related reading: JSON arrays vs objects, JSON.parse() and JSON.stringify(), and JSON in REST APIs.

The best JSON-to-TypeScript workflow is: inspect the sample, infer a draft, check the API contract, and validate incoming data where correctness matters.

Frequently Asked Questions

What is the fastest way to convert JSON to TypeScript types?
Paste a representative sample into a generator such as quicktype, which infers interfaces from the values it sees. Treat the output as a draft: a single sample cannot reveal optional fields, enum-like strings, or IDs that exceed JavaScript's safe integer range.
Should I use interface or type for JSON data?
Either works for object shapes; interfaces are conventional for records and type aliases are needed for unions such as 'pending' | 'paid'. Both are erased at compile time, so the choice has no effect on runtime validation.
How do I represent null versus a missing key in TypeScript?
Use a question mark for a key that may be absent (website?: string) and a null union for a key that is present but empty (website: string | null). Combine them only when the API genuinely sends both forms.
Can TypeScript validate JSON at runtime?
No. Types disappear during compilation, and a cast such as "as User" never inspects the data. Use a runtime validator such as zod, a hand-written type guard, or a JSON Schema validator for data that crosses a network boundary.
Can I generate TypeScript types from a JSON Schema or OpenAPI document?
Yes. json-schema-to-typescript turns a JSON Schema into interfaces, and openapi-typescript does the same for an OpenAPI description. Generating from the contract rather than from a sample captures optional fields and enums that examples leave out.

Format the sample before you type it

Paste the API response into JSON Prettifier to confirm it parses and to sort keys so nested fields line up with the interface you are writing. It runs entirely in your browser.

Format the JSON sample