← All articles

How to Generate TypeScript Interfaces from JSON

Learn how to convert JSON to TypeScript interfaces — manually, with online tools, and in code. Covers nested objects, optional fields, arrays, nullable types, and keeping types in sync with your API.

Every TypeScript developer has been in this situation: you're integrating a new API, you have a sample response, and you need typed interfaces before you can write a single line of business logic. Writing those interfaces manually is tedious, repetitive, and error-prone — especially when the response is deeply nested or has dozens of fields. This guide covers every practical approach for converting JSON to TypeScript interfaces: what the output should look like, how to generate it automatically, and how to handle tricky cases.

Why TypeScript Interfaces Matter for JSON Data

When you call an API in TypeScript without type annotations, the parsed response is any. That means no autocomplete, no type checking, and no compile-time safety. A typo in a property name becomes a runtime bug instead of a build error.

// Without types — everything is any
const response = await fetch('/api/users/42');
const user = await response.json(); // user: any
console.log(user.naem); // typo — no error at compile time

// With a typed interface
interface User {
  id: number;
  name: string;
  email: string;
}

const response = await fetch('/api/users/42');
const user: User = await response.json();
console.log(user.naem); // Error: Property 'naem' does not exist on type 'User'

The interface adds zero runtime cost — it exists only at compile time. But it changes the development experience completely.

What a JSON-to-TypeScript Conversion Produces

Every JSON object becomes a TypeScript interface, every JSON primitive maps to its TypeScript equivalent, and arrays infer their element type.

// JSON input
{
  "id": 42,
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "active": true,
  "score": 98.5,
  "address": {
    "street": "123 Main St",
    "city": "New York",
    "zip": "10001"
  },
  "tags": ["admin", "editor"],
  "orders": [
    { "id": 101, "total": 49.99, "status": "shipped" },
    { "id": 102, "total": 99.00, "status": "pending" }
  ]
}
// TypeScript output
interface Root {
  id: number;
  name: string;
  email: string;
  active: boolean;
  score: number;
  address: Address;
  tags: string[];
  orders: Order[];
}

interface Address {
  street: string;
  city: string;
  zip: string;
}

interface Order {
  id: number;
  total: number;
  status: string;
}

Each nested object becomes a named interface. The name comes from the parent key — address becomes Address, items in orders become Order. This keeps the types readable and composable.

The Type Mapping Rules

Understanding how JSON types map to TypeScript types makes the output predictable and lets you adjust it confidently.

JSON valueTypeScript type
"hello"string
42, 3.14number
true, falseboolean
nullnull
{}named interface
['a', 'b']string[]
[1, 'two'](number | string)[]
[] (empty)unknown[]

One nuance: TypeScript has no integer type. Both 42 and 3.14 become number. If the distinction matters for your use case, you can use branded types or add a comment.

Method 1: Use an Online JSON-to-TypeScript Converter

For one-off tasks — a new API endpoint, a quick prototype — an online tool is the fastest path. fixjson's JSON-to-TypeScript converter handles the full conversion in your browser:

  1. Paste your JSON into the input panel. The tool accepts valid JSON, or broken JSON with common issues like trailing commas and single quotes — it repairs the JSON before converting.
  2. TypeScript interfaces appear instantly on the right — no button to click.
  3. Copy the output and paste it directly into your TypeScript file.

Everything runs locally. No data is sent to any server — important when the JSON you're working with contains API keys, user data, or internal configuration.

Method 2: Write the Conversion in TypeScript

When you need to generate types programmatically — as part of a build script or code generator — you can write the logic yourself. Here's a minimal but complete implementation:

function jsonToTypeScript(value: unknown, rootName = 'Root'): string {
  const interfaces: string[] = [];
  const seen = new Set<string>();

  function cap(s: string) {
    return s ? s.charAt(0).toUpperCase() + s.slice(1) : 'Unknown';
  }

  function singular(name: string) {
    if (/ies$/i.test(name)) return name.slice(0, -3) + 'y';
    if (/[^s]s$/i.test(name)) return name.slice(0, -1);
    return name + 'Item';
  }

  function getType(val: unknown, name: string): string {
    if (val === null) return 'null';
    if (typeof val === 'boolean') return 'boolean';
    if (typeof val === 'number') return 'number';
    if (typeof val === 'string') return 'string';
    if (Array.isArray(val)) {
      if (val.length === 0) return 'unknown[]';
      const objs = val.filter(
        (v): v is Record<string, unknown> =>
          v !== null && typeof v === 'object' && !Array.isArray(v),
      );
      if (objs.length > 0) {
        const itemName = cap(singular(name));
        buildInterface(itemName, objs);
        return `${itemName}[]`;
      }
      const types = [...new Set(val.map((v) => getType(v, name)))];
      return types.length === 1 ? `${types[0]}[]` : `(${types.join(' | ')})[]`;
    }
    if (typeof val === 'object') {
      const iName = cap(name);
      buildInterface(iName, [val as Record<string, unknown>]);
      return iName;
    }
    return 'unknown';
  }

  function buildInterface(name: string, objs: Record<string, unknown>[]): void {
    if (seen.has(name)) return;
    seen.add(name);
    const allKeys = [...new Set(objs.flatMap((o) => Object.keys(o)))];
    const props: string[] = [];
    for (const key of allKeys) {
      const present = objs.filter((o) => key in o);
      const optional = present.length < objs.length ? '?' : '';
      const propType = getType(present[0][key], cap(key));
      const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
      props.push(`  ${safeKey}${optional}: ${propType};`);
    }
    interfaces.push(`interface ${name} {\n${props.join('\n')}\n}`);
  }

  const rootType = getType(value, rootName);
  const defs = interfaces.reverse().join('\n\n');
  if (rootType !== cap(rootName)) {
    return `type ${cap(rootName)} = ${rootType};\n\n${defs}`.trim();
  }
  return defs;
}

Usage in a Node.js build script:

import * as fs from 'fs';

const json = JSON.parse(fs.readFileSync('api-response.json', 'utf8'));
const typescript = jsonToTypeScript(json, 'UserResponse');
fs.writeFileSync('types/user.ts', typescript);

Method 3: quicktype (CLI and Library)

quicktype is the most feature-complete open-source tool for generating TypeScript types from JSON. It handles edge cases that simple generators miss and supports multiple output formats.

npm install -g quicktype

# From a local file
quicktype api-response.json -o types/user.ts --lang typescript

# From a URL
quicktype https://api.example.com/users/42 -o types/user.ts --lang typescript

# From stdin
cat api-response.json | quicktype --lang typescript

quicktype also generates runtime validation functions using libraries like io-ts or zod — useful when you need to validate incoming data, not just type it statically.

Handling Tricky Cases

Nullable fields

JSON APIs frequently return null for missing values. A generator seeing null produces null as the type — but what you usually want is string | null:

// Generated (first pass — too narrow)
interface Root {
  name: string;
  middleName: null;
}

// What you should write
interface Root {
  name: string;
  middleName: string | null;
}

Always review nullable fields manually after generation. If you have multiple sample responses where a field is sometimes a string and sometimes null, a good generator will infer string | null automatically.

Optional vs nullable

TypeScript distinguishes between field?: string (key may be absent) and field: string | null (key always present, value may be null):

interface Order {
  id: number;
  total: number;
  discount?: number;   // key absent from some responses
  note: string | null; // key always present, value may be null
}

Generators that analyze multiple JSON samples can infer this distinction accurately. Single-sample generators mark absent keys as optional but cannot distinguish a missing key from a null value.

Keys that aren't valid identifiers

Some JSON keys contain characters that aren't valid TypeScript property names:

// JSON
{ "content-type": "application/json", "x-request-id": "abc123" }

// Generated TypeScript — valid but requires bracket notation
interface Root {
  "content-type": string;
  "x-request-id": string;
}

// Access with bracket notation
obj["content-type"]

If you control the API, prefer camelCase keys to avoid this entirely. If you don't, the quoted property syntax is valid TypeScript and works correctly.

Arrays of mixed types

// JSON
{ "values": [1, "two", true, null] }

// Generated
interface Root {
  values: (number | string | boolean | null)[];
}

Union types like this are technically correct but usually signal a design problem in the API. If you see this, verify whether the field is genuinely mixed or whether the sample data is misleading.

Using Generated Types Safely

Generating an interface is only the first step. The interface is a compile-time contract — it doesn't validate anything at runtime. response.json() returns any, and TypeScript will let you cast it to your interface without actual checking.

For internal APIs where you control both sides, this is usually fine. For external APIs or user-provided data, validate at runtime. The recommended approach is Zod:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  active: z.boolean(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string(),
  }),
  tags: z.array(z.string()),
  orders: z.array(z.object({
    id: z.number(),
    total: z.number(),
    status: z.string(),
  })),
});

type User = z.infer<typeof UserSchema>; // TypeScript type derived from schema

const raw = await response.json();
const user = UserSchema.parse(raw); // throws if shape doesn't match

Zod's approach is the best of both worlds: the schema is the single source of truth for both the TypeScript type and the runtime validation. If the API changes in a breaking way, you find out at the point where the data enters your system.

Beyond Interfaces: Validators and Typed Clients

A generated interface is a compile-time contract. The next step up is libraries that give you both the type and the runtime check from a single source:

  • Zod and io-ts — define a schema, infer the TypeScript type from it, and validate incoming data at the boundary. If the API changes, your build (and your error logs) tell you immediately.
  • tRPC and typed fetch wrappers (e.g. openapi-fetch) — the server's procedure or OpenAPI definition is the source of truth, and the client picks up matching types automatically, no JSON-to-TS step in between.
  • ts-pattern and discriminated unions — once your data has a tag field (e.g. { kind: "circle" | "square", ... }), exhaustive pattern matching catches missed cases at compile time.

Keeping Types in Sync with the API

Generated types have a shelf life. APIs change — a field gets renamed, a new required field appears, or a string field becomes nullable. Your interfaces become stale silently.

Generate from the OpenAPI spec, not from sample responses

If the API has an OpenAPI (Swagger) definition, openapi-typescript generates types directly from the spec:

npm install -g openapi-typescript
openapi-typescript https://api.example.com/openapi.json -o types/api.ts

This is far more reliable than generating from a sample response, because the spec documents every field including optional ones, nullable variants, and enum values.

Regenerate types in CI

Add a step to your CI pipeline that regenerates types from the spec or a known-good sample, then runs tsc --noEmit to check for breakages. If the API changes, the build fails before the breaking change reaches production.

Frequently Asked Questions

How do I convert JSON to a TypeScript interface?

Paste the JSON into an online converter like fixjson's JSON-to-TypeScript tool for instant output, run quicktype in a build step, or generate from an OpenAPI spec with openapi-typescript for long-lived code.

How do JSON types map to TypeScript types?

Strings → string, all numbers → number (TypeScript has no integer type), booleans → boolean, objects → named interfaces, and arrays infer their element type. See the mapping table above and the six JSON data types.

How do I handle nullable or optional fields?

Use field?: T when the key may be absent and field: T | null when the key is always present but the value can be null. Generators that analyse multiple samples infer this; single-sample tools need manual review.

Do TypeScript interfaces validate JSON at runtime?

No — interfaces are erased at compile time. For runtime safety on external data, validate with Zod or describe the shape with JSON Schema.

Conclusion

Converting JSON to TypeScript interfaces is something every TypeScript developer does regularly. The right approach depends on context: for a quick one-off integration, an online converter gets you running in under a minute. For a build pipeline, quicktype or a custom script automates the process. For long-lived production code, generating types from an OpenAPI spec and validating at runtime with Zod gives you the strongest guarantees.

Whatever method you use, the generated interface is a starting point — review nullable fields, add comments for non-obvious properties, and tighten constraints where your business rules are stricter than what the JSON sample implies. Types that accurately reflect your data make the entire codebase easier to maintain.

If the JSON you're working with has syntax errors before you even try to convert it, fix it first. A TypeScript generator — like any JSON parser — requires syntactically valid input.