Home → Blog → JSON to TypeScript Guide
JSON to TypeScript: Generate Type-Safe Interfaces Automatically (2026)
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:
- Access a property that does not exist (typos, renamed fields)
- Treat a nullable field as always-present
- Pass the wrong data shape to a function
- Destructure a field that might be
undefinedin some API responses
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) | 42 | number |
| Number (float) | 3.14 | number |
| Boolean | true / false | boolean |
| Null | null | null |
| 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:
- OpenAPI / Swagger — If your API has an OpenAPI spec, generate TypeScript types from the spec rather than from sample JSON. Tools like
openapi-typescriptgenerate complete, accurate types directly from your API definition. - Zod schema as source of truth — Define your schema once in Zod, infer the TypeScript type from it (
type User = z.infer<typeof UserSchema>), and validate API responses against the schema at runtime. The JSON to Zod converter generates a Zod schema from your JSON sample. - CI type-check integration — Add a step in your CI pipeline that fetches a live API response and runs the TypeScript compiler against it to catch type mismatches early.
- Contract testing — Use a tool like Pact to define API contracts that both the frontend and backend verify in their respective test suites.
// 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
quicktype -s json -o types.ts --lang typescript sample.json.interface for object shapes from APIs and type for unions. Both work equally well with JSON.parse() results and with generics.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.? 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.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