TypeScript Types from API JSON: A Complete Guide

10 min readTypeScript & JSON

Introduction

When you consume a REST API in TypeScript, you need types that match the JSON response structure. Writing these types manually is tedious and error-prone - one wrong field name and your types silently diverge from reality. This guide shows you how to generate TypeScript types from JSON data automatically, and how to add runtime validation with Zod for end-to-end type safety.

The Problem: Manual Types Drift

Consider a typical API response:

// API response from /api/users/123
{
  "id": 1,
  "name": "Alice",
  "email": "[email protected]",
  "isActive": true,
  "address": {
    "street": "123 Main St",
    "city": "San Francisco",
    "zipCode": "94105"
  },
  "roles": ["admin", "user"]
}

Writing the TypeScript interface manually:

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  address: {
    street: string;
    city: string;
    zipCode: string;
  };
  roles: string[];
}

This works, but what happens when the API adds a phoneNumber field? Or changeszipCode to postalCode? Your TypeScript types silently drift from the actual API response. No compile error, no runtime error - just wrong types.

Solution 1: Auto-Generate TypeScript Types

Instead of writing types by hand, generate them from actual JSON data. You can use ourJSON to TypeScript toolor command-line tools.

Using the Online Tool

  1. Paste your API JSON response into the JSON to TypeScript tool
  2. Set the interface name (e.g., User, Product, Order)
  3. Choose TypeScript interface or type
  4. Click Generate and copy the output

Using quicktype (CLI)

# Install quicktype
npm install -g quicktype

# Generate TypeScript from a JSON file
quicktype input.json -o user.ts --lang ts

# Generate from a URL
quicktype https://api.example.com/users/1 -o user.ts --lang ts

Solution 2: Runtime Validation with Zod

TypeScript types only exist at compile time. If an API returns unexpected data, your types won't catch it at runtime. Zod solves this by validating data at runtime while inferring TypeScript types automatically.

import { z } from "zod";

// Define the schema (also generates TypeScript types)
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  isActive: z.boolean(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zipCode: z.string(),
  }),
  roles: z.array(z.string()),
});

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

// Use it to validate API responses
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // This throws if the data doesn't match the schema
  return UserSchema.parse(data);
}

With Zod, you get both compile-time types and runtime validation. If the API changes its response format, UserSchema.parse() will throw a clear error instead of silently accepting wrong data.

Best Practices

  • Always validate API responses - Never trust external data. Use Zod schemas at API boundaries.
  • Use z.infer for types - Define schemas once, derive types automatically. Single source of truth.
  • Make optional fields explicit - Use z.optional() or z.nullable() for fields that may be missing.
  • Generate types from real data - Use actual API responses, not imagined structures.
  • Regenerate when APIs change - When the API updates, regenerate types and check for differences.
  • Use z.coerce for form data - HTML forms and query params return strings. z.coerce.number() converts automatically.

Common Patterns

Optional and Nullable Fields

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email().optional(),    // may be undefined
  phone: z.string().nullable(),             // may be null
  avatar: z.string().url().optional(),      // may be undefined
});

Discriminated Unions

const SuccessResponse = z.object({
  status: z.literal("success"),
  data: z.array(UserSchema),
});

const ErrorResponse = z.object({
  status: z.literal("error"),
  message: z.string(),
  code: z.number(),
});

const ApiResponse = z.discriminatedUnion("status", [
  SuccessResponse,
  ErrorResponse,
]);

Transform API Data

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  created_at: z.string().transform(str => new Date(str)),
  is_active: z.boolean().transform(val => val ? "Active" : "Inactive"),
});

type User = z.infer<typeof UserSchema>;
// created_at is now Date, is_active is now "Active" | "Inactive"

Tools for Generating TypeScript from JSON

ToolTypeZod SupportBest For
ByteJSON ToolOnline (free)YesQuick one-off generation
quicktypeCLI / OnlineNoMulti-language generation
json-schema-to-zodCLI / LibraryYes (output)From JSON Schema to Zod
openapi-typescriptCLINoFrom OpenAPI specs

Key Takeaways

  • Never manually write TypeScript types for API responses - generate them from real data
  • TypeScript types alone don't protect you at runtime - add Zod validation at API boundaries
  • Use z.infer<typeof Schema> to derive types from Zod schemas (single source of truth)
  • Regenerate types when APIs change to catch breaking changes early
  • Start with our JSON to TypeScript tool for instant type generation

Related Resources