HomeBlog → JSON to TypeScript Guide

JSON to TypeScript: Generate Type-Safe Interfaces Automatically (2026)

📅 Updated April 2026 ⏱ 12 min read 🛠 TypeScript guide

Every TypeScript developer has been there: you receive a JSON response from an API, assign it to a variable typed as any, and move on. It works — until a backend developer renames a field, returns null where you expected a string, or adds an unexpected level of nesting, and your runtime error surfaces three sprints later. Generating TypeScript interfaces from your JSON eliminates this entire class of bugs at compile time. This guide explains how the type inference process works, covers every significant edge case, and shows you how to use the generated types effectively in real-world React, fetch, and state management code.

Convert JSON to TypeScript instantly

Paste your JSON and get TypeScript interfaces with proper types, nested objects, and optional fields.

Open JSON to TypeScript Tool →

Why Convert JSON to TypeScript Interfaces?

TypeScript's primary value proposition is catching errors at compile time that would otherwise surface at runtime. When you work with JSON data — whether from REST APIs, configuration files, or localStorage — that data enters your application as an untyped blob. Without an interface, TypeScript has no way to warn you when you:

Using any to silence TypeScript complaints is the most common anti-pattern. It gives you the syntax of TypeScript without any of the safety. The difference in developer experience between an untyped API response and a fully typed one with proper interfaces is enormous — IntelliSense autocomplete, refactoring safety, and instant error feedback are only available when TypeScript knows the shape of your data.

Manually writing interfaces from large API responses is tedious and error-prone. Automated generation from a sample JSON response takes seconds and produces correct types for every field in the document.

How Type Inference Works

When converting JSON to TypeScript, the type inference algorithm maps each JSON value type to its TypeScript equivalent:

JSON Type Example JSON Value TypeScript Type
String"hello"string
Number (integer)42number
Number (float)3.14number
Booleantrue / falseboolean
Nullnullnull
Object{"key": "value"}Named interface
Array of strings["a", "b"]string[]
Array of numbers[1, 2, 3]number[]
Array of objects[{…}, {…}]ItemType[]
Mixed array["a", 1, true](string | number | boolean)[]
Empty array[]unknown[]

TypeScript does not distinguish between integer and float — both are number. JSON does not have a Date type; date strings like "2026-04-01T12:00:00Z" are inferred as string, and it is up to you to parse them with new Date() where needed.

Simple Object Example

Let us start with a basic flat JSON object — a single user record from an API response:

Input JSON

{
  "id": 1,
  "username": "alice",
  "email": "alice@example.com",
  "age": 28,
  "verified": true,
  "score": 4.8
}

Generated TypeScript

interface Root {
  id: number;
  username: string;
  email: string;
  age: number;
  verified: boolean;
  score: number;
}

All six fields get explicit types based on their JSON values. The interface is ready to use immediately as a type annotation anywhere in your codebase. You would typically rename Root to something meaningful like User to reflect the domain entity.

Nested Objects

When a JSON object contains nested objects, the converter creates separate named interfaces for each nesting level. This keeps the type definitions readable and reusable:

Input JSON

{
  "user": {
    "id": 1,
    "name": "Alice",
    "address": {
      "street": "123 Main St",
      "city": "Springfield",
      "zip": "62701"
    }
  }
}

Generated TypeScript

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

interface User {
  id: number;
  name: string;
  address: Address;
}

interface Root {
  user: User;
}

Each nested object becomes its own interface. The name is derived from the JSON key by capitalizing the first letter. This approach allows you to reuse Address anywhere else in your code, not just inside User. For very deep nesting (four or more levels), consider flattening the structure or using index signatures for dynamic keys.

Arrays of Objects

When a JSON key contains an array of objects, the converter infers the item type and generates both a per-item interface and a typed array:

Input JSON

{
  "products": [
    {
      "id": 1,
      "name": "Widget",
      "price": 9.99,
      "inStock": true
    },
    {
      "id": 2,
      "name": "Gadget",
      "price": 24.99,
      "inStock": false
    }
  ]
}

Generated TypeScript

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

interface Root {
  products: Product[];
}

The converter samples all items in the array and merges their schemas. If the first item has field description and the second does not, the generated interface marks description as optional: description?: string. This is why providing multiple representative sample items rather than a single item produces more accurate types.

Optional Fields and Null Values

Two related but distinct situations require careful handling: fields that are sometimes absent, and fields that are present but null.

Optional Fields

A field is optional when it may or may not appear in the JSON response. In TypeScript, this is expressed with the ? modifier:

interface User {
  id: number;
  name: string;
  bio?: string;          // May be absent entirely
  avatarUrl?: string;    // May be absent entirely
}

Nullable Fields

A field is nullable when it is always present but its value can be null. In TypeScript, this is expressed as a union with null:

interface User {
  id: number;
  name: string;
  bio: string | null;       // Always present, but can be null
  lastLoginAt: string | null; // Always present, but can be null
}

Both Optional and Nullable

When a field can be either absent or null, combine both:

interface User {
  id: number;
  name: string;
  bio?: string | null;    // May be absent OR null
}

Tip: When generating types from a single JSON sample, the tool cannot know whether a non-null field is always non-null or just happens to be non-null in this sample. For fields from a database that allows NULL, always review the generated types and add | null where appropriate.

Mixed Type Arrays

JSON arrays can contain mixed types. TypeScript handles this with union types:

// JSON input
{
  "mixed": [1, "two", true, null],
  "ids": [1, 2, 3],
  "tags": ["admin", "editor", "viewer"]
}

// Generated TypeScript
interface Root {
  mixed: (number | string | boolean | null)[];
  ids: number[];
  tags: string[];
}

In practice, truly mixed arrays are unusual in well-designed APIs. If you encounter one, consider whether you should be using a discriminated union or a more specific type rather than accepting any element type. The generated union type is correct and type-safe — you just need to narrow it before use:

const items: (number | string)[] = [1, "two", 3];

items.forEach(item => {
  if (typeof item === "number") {
    console.log(item * 2);   // TypeScript knows item is number here
  } else {
    console.log(item.toUpperCase()); // TypeScript knows item is string here
  }
});

Readonly and Strict Types

For data that comes from an API and should not be mutated, consider adding the readonly modifier to your interfaces. This prevents accidental mutation and makes your data flow intention explicit:

// Regular interface — mutable
interface User {
  id: number;
  name: string;
  roles: string[];
}

// Readonly interface — immutable
interface ReadonlyUser {
  readonly id: number;
  readonly name: string;
  readonly roles: readonly string[];
}

// Utility type approach — converts all fields to readonly
type ImmutableUser = Readonly<User>;

// Deep readonly using a recursive type (for nested objects)
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

For strictNullChecks mode (enabled in any strict TypeScript config), all nullable fields must explicitly include | null in their type — TypeScript will not allow you to assign null to a string field without it. Always develop with "strict": true in your tsconfig.json.

Using Generated Types in Practice

Typing fetch() Responses

The fetch() API's response.json() returns Promise<any>. Use a type assertion to apply your generated interface:

interface ApiResponse {
  users: User[];
  total: number;
  page: number;
}

async function getUsers(page: number): Promise<ApiResponse> {
  const response = await fetch(`/api/users?page=${page}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  // Type assertion — you trust the API matches this shape
  return response.json() as Promise<ApiResponse>;
}

// Usage — fully typed
const data = await getUsers(1);
console.log(data.users[0].name); // TypeScript knows this is a string
console.log(data.total);         // TypeScript knows this is a number

React State Typing

import { useState, useEffect } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch('/api/products')
      .then(r => r.json() as Promise<Product[]>)
      .then(data => { setProducts(data); setLoading(false); })
      .catch(e => { setError(e.message); setLoading(false); });
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>{p.name} — ${p.price}</li>
      ))}
    </ul>
  );
}

TypeScript Generics for API Responses

Most REST APIs wrap their response data in a standard envelope structure. Instead of duplicating this wrapper in every interface, create a generic ApiResponse<T> type:

// Generic API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

// Paginated wrapper
interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  perPage: number;
  hasNext: boolean;
}

// Usage with your generated interfaces
type UserResponse = ApiResponse<User>;
type ProductListResponse = PaginatedResponse<Product>;

// Generic fetch utility
async function apiFetch<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return res.json() as Promise<ApiResponse<T>>;
}

// Fully typed call
const result = await apiFetch<User[]>('/api/users');
result.data.forEach(user => console.log(user.name)); // user is User

This pattern pays dividends at scale — when your API adds a new field to the response envelope (such as a requestId for tracing), you update it in one place and the change propagates to all endpoints automatically.

Keeping Types in Sync with Your API

Generated types become stale as your API evolves. The longer you wait between updates, the bigger the drift. Here are strategies for keeping types synchronized:

// Zod approach — runtime validation + compile-time types from a single definition
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
  role: z.enum(['admin', 'editor', 'viewer']),
  bio: z.string().nullable().optional(),
});

// TypeScript type is inferred automatically — no duplication
type User = z.infer<typeof UserSchema>;

// Runtime validation of the API response
async function getUser(id: number): Promise<User> {
  const raw = await fetch(`/api/users/${id}`).then(r => r.json());
  return UserSchema.parse(raw); // Throws if the shape doesn't match
}

Common TypeScript Typing Mistakes

Using any Instead of Unknown

The any type completely disables TypeScript's type checking for a value and all its properties. It is contagious — accessing a property of an any value yields any, silently spreading the lack of type safety through your codebase. Prefer unknown for truly unknown data, which forces you to check the type before use:

❌ Bad — any propagates silently throughout

const data: any = await response.json();
const name = data.user.profile.name; // No error even if user is undefined

✅ Better — use a proper interface or unknown with validation

const data = await response.json() as UserApiResponse;
const name = data.user.profile.name; // TypeScript checks every access

Not Handling Null Values

When you generate types from a JSON sample where all fields happen to be non-null, the generated interface will not include | null unions. But if the API can actually return null for any field, your code will fail at runtime when it does. Always review generated types for fields that come from nullable database columns or optional API parameters.

Overly Broad Types

Typing a field as string when it can only be one of a few specific values misses an opportunity for stronger typing. Use string literal union types for enums:

// Too broad
interface Order {
  status: string; // Could be anything
}

// Much better — TypeScript enforces valid values
interface Order {
  status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
}

Frequently Asked Questions

How do I convert JSON to TypeScript interfaces? +
Paste your JSON into the JSON to TypeScript tool at jsonwebtools.com and click Convert. The tool analyzes each field's type and generates named TypeScript interfaces with correct types for strings, numbers, booleans, nested objects, arrays, and null values. You can also use the quicktype CLI: quicktype -s json -o types.ts --lang typescript sample.json.
What is the difference between a TypeScript interface and type alias for JSON data? +
For JSON object shapes, interfaces and type aliases are largely interchangeable. Interfaces support declaration merging, while type aliases are more flexible for union types and mapped types. Most style guides prefer interface for object shapes from APIs and type for unions. Both work equally well with JSON.parse() results and with generics.
How do I type the result of JSON.parse() in TypeScript? +
JSON.parse() returns any in TypeScript. For a quick type assertion: const data = JSON.parse(text) as MyInterface. For runtime safety, use a validation library like Zod: const data = MySchema.parse(JSON.parse(text)) — this both infers the TypeScript type and validates the shape at runtime, catching API contract violations early.
How do I handle optional fields when generating TypeScript from JSON? +
Fields that may be absent should use the ? modifier: fieldName?: string. Fields that are present but can be null should use a union: fieldName: string | null. Fields that can be both absent and null: fieldName?: string | null. When generating from a single sample, review the output and add | null for any fields that can be null in your API.
Should I use any or unknown for JSON data in TypeScript? +
Prefer unknown over any for untyped JSON data. With unknown, TypeScript forces you to narrow the type before using a value, preventing runtime errors. With any, TypeScript silently disables all type checking. The best approach is to generate a specific interface from your JSON sample and use it directly, avoiding both any and unknown except at the parse boundary.

Related Tools & Guides

JSON to TypeScript Tool  |  JSON Schema Validator  |  JSON Validator  |  JSON to Zod  |  JSON to Python