AllJSONTools

Free JSON Developer Tools

Convert JSON to TypeScript: Generate Types and Interfaces from API Responses

2026-02-26 · 13 min read · By AllJSONTools

TypeScript
JSON
Code Generation
Having JSON issues?

Paste broken JSON and fix it instantly with AI — plain-English explanations included.

Fix JSON with AI

Why TypeScript Types from JSON Matter

Every modern web application consumes JSON from APIs, configuration files, or databases. The moment that JSON crosses into your TypeScript codebase, you face a choice: treat it as an opaque any and lose every safety guarantee TypeScript provides, or define precise types that describe the exact shape of your data.

Well-defined types give you autocomplete in your editor, compile-time error detection when an API response changes, self-documenting code that new team members can read without consulting external docs, and refactoring confidence across your entire codebase. The cost of not typing your JSON is bugs that slip through to production — the kind where user.address.city is suddenly undefined because the backend renamed the field to locality.

This guide walks through every technique for converting JSON to TypeScript types — from manual approaches to automated tooling — with real-world examples you can apply immediately. If you work with APIs and TypeScript, you will find practical patterns you can use in your next project. For background on working with JSON in JavaScript in general, see the guide to parsing JSON in JavaScript.

The Manual Approach: Writing Interfaces by Hand

Suppose your API returns a user object like this:

json
{
  "id": 1042,
  "username": "jdoe",
  "email": "jdoe@example.com",
  "profile": {
    "firstName": "Jane",
    "lastName": "Doe",
    "bio": "Software engineer and open-source contributor",
    "avatarUrl": "https://cdn.example.com/avatars/jdoe.jpg"
  },
  "roles": ["admin", "editor"],
  "createdAt": "2025-08-14T09:30:00Z",
  "lastLoginAt": "2026-02-25T14:22:11Z"
}

Writing the interface by hand looks like this:

typescript
interface UserProfile {
  firstName: string;
  lastName: string;
  bio: string;
  avatarUrl: string;
}

interface User {
  id: number;
  username: string;
  email: string;
  profile: UserProfile;
  roles: string[];
  createdAt: string;
  lastLoginAt: string;
}

For a simple payload like this, hand-writing is manageable. But in reality, API responses often contain dozens of fields, deeply nested objects, arrays of polymorphic items, and optional fields that only appear in certain conditions. Manually typing a response with 40 or 50 fields is tedious, error-prone, and hard to keep in sync when the API evolves. A single typo — writing avatarURL instead of avatarUrl — silently introduces a bug that TypeScript cannot catch because the type itself is wrong.

Using Automated Tools: Paste JSON, Get TypeScript

The fastest and most reliable way to generate TypeScript types from JSON is to use an automated converter. Instead of reading a JSON payload line by line and mentally mapping each value to a TypeScript type, you paste your JSON and get correct interfaces instantly.

The JSON to TypeScript tool on AllJSONTools does exactly this. Paste in a raw API response and it produces clean, properly nested interfaces with correct types inferred from the values. It handles nested objects, arrays, nulls, and mixed types automatically.

Here is a typical workflow:

  1. Fetch a sample response from your API (using your browser DevTools, curl, or Postman).

  2. If the JSON is minified or messy, clean it up with the JSON Formatter first.

  3. Paste the formatted JSON into the JSON to TypeScript converter.

  4. Copy the generated interfaces into your codebase and refine as needed (rename root interface, mark optional fields, add JSDoc comments).

This approach eliminates typos, saves significant time on large payloads, and gives you a correct starting point that you can then customize.

Interfaces vs Type Aliases

TypeScript gives you two ways to describe the shape of an object: interface and type. For JSON-derived types, both work. Here are the practical differences:

typescript
// Interface — can be extended and merged
interface User {
  id: number;
  username: string;
  email: string;
}

// Extending an interface
interface AdminUser extends User {
  permissions: string[];
  department: string;
}

// Declaration merging (interfaces only)
// If a library declares User, you can add fields:
interface User {
  avatarUrl?: string;
}
typescript
// Type alias — more flexible for unions, intersections, and computed types
type User = {
  id: number;
  username: string;
  email: string;
};

// Intersection (similar to extends)
type AdminUser = User & {
  permissions: string[];
  department: string;
};

// Type aliases can do things interfaces cannot:
type Role = "admin" | "editor" | "viewer";
type UserOrError = User | { error: string; code: number };
type ReadonlyUser = Readonly<User>;

When to use each: Use interface when you are modeling object shapes that may be extended by other parts of your codebase or by third-party code. Use type when you need unions, intersections, mapped types, or conditional types. For plain API response shapes, either works — pick one convention and be consistent across your project.

Handling Optional Fields

Real-world JSON rarely has every field present on every response. Some fields appear only when relevant, some are explicitly null, and others are omitted entirely. TypeScript has distinct tools for each scenario:

typescript
// Optional property — the field may or may not be present
interface User {
  id: number;
  username: string;
  bio?: string;              // string | undefined
}

// Nullable property — the field is always present but can be null
interface User {
  id: number;
  username: string;
  deletedAt: string | null;  // always present, but null if active
}

// Optional AND nullable — the field may be absent, or present as null
interface User {
  id: number;
  username: string;
  middleName?: string | null; // can be missing, null, or a string
}

TypeScript also provides utility types that help when you need to make entire interfaces optional or required:

typescript
interface UserProfile {
  firstName: string;
  lastName: string;
  bio: string;
  avatarUrl: string;
}

// All fields become optional — useful for PATCH endpoints
type UpdateProfilePayload = Partial<UserProfile>;
// Equivalent to:
// {
//   firstName?: string;
//   lastName?: string;
//   bio?: string;
//   avatarUrl?: string;
// }

// All fields become required — useful when you know the data is complete
type CompleteProfile = Required<UserProfile>;

// Pick specific fields
type ProfileSummary = Pick<UserProfile, "firstName" | "lastName">;

When converting JSON to TypeScript, pay close attention to fields that are null in your sample data. A null value in JSON might mean "this field is always present but nullable" or "this field happens to be empty in this sample." Check your API documentation or test with multiple responses to determine the correct optionality.

Nested Objects and Arrays

Most API responses are not flat. They contain nested objects, arrays of objects, and arrays within arrays. Here is a realistic e-commerce order response and its TypeScript representation:

json
{
  "orderId": "ORD-2026-7891",
  "status": "shipped",
  "customer": {
    "id": 504,
    "name": "Alice Chen",
    "email": "alice@example.com",
    "shippingAddress": {
      "street": "742 Evergreen Terrace",
      "city": "Springfield",
      "state": "IL",
      "zip": "62704",
      "country": "US"
    }
  },
  "items": [
    {
      "productId": "PROD-001",
      "name": "Mechanical Keyboard",
      "quantity": 1,
      "unitPrice": 89.99,
      "tags": ["electronics", "peripherals"]
    },
    {
      "productId": "PROD-044",
      "name": "USB-C Cable",
      "quantity": 3,
      "unitPrice": 12.50,
      "tags": ["accessories"]
    }
  ],
  "totals": {
    "subtotal": 127.49,
    "tax": 10.20,
    "shipping": 5.99,
    "total": 143.68
  },
  "trackingNumbers": ["1Z999AA10123456784"]
}

The corresponding TypeScript interfaces break each nested object into its own named type:

typescript
interface Address {
  street: string;
  city: string;
  state: string;
  zip: string;
  country: string;
}

interface Customer {
  id: number;
  name: string;
  email: string;
  shippingAddress: Address;
}

interface OrderItem {
  productId: string;
  name: string;
  quantity: number;
  unitPrice: number;
  tags: string[];
}

interface OrderTotals {
  subtotal: number;
  tax: number;
  shipping: number;
  total: number;
}

interface Order {
  orderId: string;
  status: string;
  customer: Customer;
  items: OrderItem[];
  totals: OrderTotals;
  trackingNumbers: string[];
}

Notice how each level of nesting becomes a separate interface. This is not just good practice — it makes each type reusable. The Address interface might be used for both shipping and billing addresses. OrderItem might be reused for cart items. To visualize the structure of deeply nested JSON before converting it, try the JSON Tree Viewer.

Union Types from JSON

Sometimes a JSON field can hold values of different types depending on context. A notifications API might return different shapes based on the notification type:

json
[
  {
    "type": "message",
    "id": 1,
    "from": "alice",
    "text": "Hey, are you coming to the standup?"
  },
  {
    "type": "follow",
    "id": 2,
    "from": "bob"
  },
  {
    "type": "payment",
    "id": 3,
    "amount": 49.99,
    "currency": "USD",
    "status": "completed"
  }
]

In TypeScript, this is modeled as a discriminated union — a union of interfaces that share a common literal field (the discriminant):

typescript
interface MessageNotification {
  type: "message";
  id: number;
  from: string;
  text: string;
}

interface FollowNotification {
  type: "follow";
  id: number;
  from: string;
}

interface PaymentNotification {
  type: "payment";
  id: number;
  amount: number;
  currency: string;
  status: string;
}

type Notification =
  | MessageNotification
  | FollowNotification
  | PaymentNotification;

// TypeScript narrows the type automatically when you check the discriminant
function handleNotification(n: Notification) {
  switch (n.type) {
    case "message":
      console.log(n.text);   // TypeScript knows 'text' exists here
      break;
    case "follow":
      console.log(n.from);   // Only 'from' and 'id' available here
      break;
    case "payment":
      console.log(n.amount); // 'amount', 'currency', 'status' available
      break;
  }
}

Discriminated unions are one of TypeScript’s most powerful features for modeling JSON that contains polymorphic data. The key is identifying the field that acts as the discriminant — typically called type, kind, or status — and assigning it a string literal type in each variant. For best practices on designing APIs that produce clean discriminated unions, see the JSON API best practices guide.

Enums and Literal Types

When your JSON contains fields with a known set of string values, you can capture those as TypeScript string literal unions or enums instead of using a generic string type. Consider this API response:

json
{
  "id": 301,
  "title": "Fix login bug",
  "priority": "high",
  "status": "in_progress",
  "assignee": "jdoe"
}

If you know the valid values for priority and status, you can lock them down:

typescript
// Approach 1: String literal union (recommended for JSON-derived types)
type Priority = "low" | "medium" | "high" | "critical";
type TicketStatus = "open" | "in_progress" | "review" | "closed";

interface Ticket {
  id: number;
  title: string;
  priority: Priority;
  status: TicketStatus;
  assignee: string;
}

// Approach 2: TypeScript enum
enum Priority {
  Low = "low",
  Medium = "medium",
  High = "high",
  Critical = "critical",
}

enum TicketStatus {
  Open = "open",
  InProgress = "in_progress",
  Review = "review",
  Closed = "closed",
}

interface Ticket {
  id: number;
  title: string;
  priority: Priority;
  status: TicketStatus;
  assignee: string;
}

String literal unions vs enums: For typing JSON data, string literal unions are generally preferred. They produce no runtime JavaScript code (they are erased during compilation), they work naturally with JSON values (which are already strings), and they play well with discriminated unions. Enums add runtime objects and require you to import and reference the enum everywhere. Use enums when you need reverse mapping (number enums) or when your team convention requires them.

Runtime Validation with Zod

TypeScript types are erased at compile time. This means that at runtime, your application has no way to verify that the JSON it received actually matches the interface you declared. If the API sends unexpected data, TypeScript will not save you — the code will run, but with wrong values, leading to subtle bugs or crashes downstream.

This is where runtime validation libraries like Zod come in. Zod lets you define a schema that is both a runtime validator and a TypeScript type source:

typescript
import { z } from "zod";

// Define the schema — this runs at runtime
const OrderItemSchema = z.object({
  productId: z.string(),
  name: z.string(),
  quantity: z.number().int().positive(),
  unitPrice: z.number().nonnegative(),
  tags: z.array(z.string()),
});

const OrderSchema = z.object({
  orderId: z.string(),
  status: z.enum(["pending", "shipped", "delivered", "cancelled"]),
  items: z.array(OrderItemSchema),
  totals: z.object({
    subtotal: z.number(),
    tax: z.number(),
    shipping: z.number(),
    total: z.number(),
  }),
});

// Infer the TypeScript type from the schema — no duplication
type Order = z.infer<typeof OrderSchema>;

// Use it to validate API responses at runtime
async function fetchOrder(id: string): Promise<Order> {
  const response = await fetch(`/api/orders/${id}`);
  const json = await response.json();

  // This throws a ZodError with detailed messages if validation fails
  return OrderSchema.parse(json);
}

// Or use safeParse for non-throwing validation
async function safeFetchOrder(id: string) {
  const response = await fetch(`/api/orders/${id}`);
  const json = await response.json();
  const result = OrderSchema.safeParse(json);

  if (!result.success) {
    console.error("Validation errors:", result.error.issues);
    return null;
  }

  return result.data; // correctly typed as Order
}

The JSON to Zod tool generates Zod schemas directly from JSON, so you do not have to write them by hand. Paste your API response and get a complete Zod schema with inferred types. This is particularly valuable for APIs you do not control, where the response shape might change without warning.

End-to-End API Response Typing Workflow

Here is the complete workflow for going from an unknown API response to fully typed, runtime-validated TypeScript code:

Step 1: Capture and Format the Response

Fetch a real response from your API. If it is minified, paste it into the JSON Formatter to make it readable. Inspect the structure visually using the JSON Tree Viewer to understand the nesting before you start typing.

Step 2: Generate TypeScript Interfaces

Paste the formatted JSON into the JSON to TypeScript converter. Review the output: rename the root interface to something meaningful (e.g., ApiUserResponse instead of Root), mark fields as optional where appropriate, and replace generic string types with literal unions for known values.

Step 3: Add Runtime Validation

For APIs you do not control (third-party services, public APIs), generate a Zod schema using the JSON to Zod tool. Use z.infer to derive the TypeScript type from the Zod schema, keeping a single source of truth.

Step 4: Create a Typed Fetch Function

typescript
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  username: z.string(),
  email: z.string(),
  profile: z.object({
    firstName: z.string(),
    lastName: z.string(),
    bio: z.string().nullable(),
    avatarUrl: z.string().nullable(),
  }),
  roles: z.array(z.string()),
  createdAt: z.string(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`Failed to fetch user: HTTP ${response.status}`);
  }

  const json = await response.json();
  return UserSchema.parse(json);
}

// Usage — fully typed with runtime safety
const user = await fetchUser(1042);
console.log(user.profile.firstName); // autocomplete works
console.log(user.profile.bio);       // typed as string | null

Alternatively, if you prefer a schema-first approach, you can generate a JSON Schema from your data using the JSON Schema Generator and then derive both validation logic and TypeScript types from that schema. Read more about this approach in the JSON Schema explained guide.

Common Pitfalls

  1. The any escape hatch — It is tempting to type an API response as any to "get it working" and come back later. You will not come back later. Every any is a hole in your type safety. Use unknown instead and narrow with validation.

  2. Trusting API shapes blindly — Casting response.json() as User tells TypeScript to trust you, but it performs zero validation. If the API changes its response shape, your code will compile fine and break at runtime. Always validate at the boundary.

  3. Date serialization — JSON has no Date type. Dates are serialized as ISO 8601 strings ( "2026-02-25T14:22:11Z"). If you type a field as Date in your interface, TypeScript will not complain when you assign a string to it via JSON.parse() (because the result is any), but calling .getFullYear() on it will crash. Type date fields as string and convert them explicitly with new Date() or use Zod’s z.coerce.date() transform.

  4. Generating types from a single sample — One API response is not always representative. A field might be an empty array in one response and an array of objects in another. A field that looks like a number might occasionally be a string. Always test with multiple responses, or better yet, generate types from the API’s schema if one exists.

  5. Ignoring null vs undefined — In JSON, null is an explicit value while absent keys are just missing. TypeScript distinguishes between null and undefined. Model them correctly in your types to avoid surprises.

typescript
// BAD — trusting the API blindly with a type assertion
const user = (await response.json()) as User;
user.profile.bio.toUpperCase(); // Runtime crash if bio is null

// GOOD — validate first, then use the typed result
const json: unknown = await response.json();
const user = UserSchema.parse(json);
// Now 'user' is guaranteed to match the schema at runtime

Summary and Recommended Workflow

Converting JSON to TypeScript is not just about generating interfaces — it is about building a reliable pipeline from raw API data to fully typed, validated application state. Here is the workflow we recommend:

  1. Format first. Clean up your raw JSON with the JSON Formatter so you can see the full structure clearly.

  2. Visualize the structure. For complex responses, use the JSON Tree Viewer to understand nesting and spot arrays of objects.

  3. Generate TypeScript types. Use the JSON to TypeScript tool for instant, accurate interface generation. Refine the output: rename interfaces, mark optional fields, and narrow string fields to literal unions.

  4. Add runtime validation. Generate a Zod schema with JSON to Zod for data you do not control. Use z.infer as the single source of truth for both types and validation.

  5. Consider a schema-first approach. For APIs you own, define a JSON Schema first, generate it with the JSON Schema Generator, and derive both your TypeScript types and your API validation from that single schema definition.

  6. Never use any. Use unknown for unvalidated data and narrow it with type guards or Zod.

By combining compile-time types with runtime validation, you create a defense-in-depth strategy where TypeScript catches structural errors during development and Zod catches data errors in production. The tools on AllJSONTools automate the tedious parts of this workflow so you can focus on building your application logic.

Having JSON issues?

Paste broken JSON and fix it instantly with AI — plain-English explanations included.

Fix JSON with AI