JSON to TypeScript Interface: Tools, Manual Patterns, and Best Practices
Last updated:
Working with a JSON API in TypeScript? Converting the response shape to a typed interface prevents runtime errors and unlocks IDE autocomplete. This guide covers quicktype (best tool), json-to-ts, manual conversion patterns, nullable vs optional, and how TypeScript interfaces compare to JSON Schema.
1. JSON Type to TypeScript Type Mapping
| JSON value | TypeScript type | Notes |
|---|---|---|
"Alice" | string | |
42 | number | Integers and floats are both number |
true | boolean | |
null | null | Use string | null for nullable strings |
{"[1, 2, 3]"} | number[] | |
{"[{...}, {...}]"} | T[] | T is a named interface |
{"{ key: val }"} | interface | or Record<string, T> |
| absent field | property?: T | Optional (T | undefined) |
2. quicktype — Best Tool
# Install
npm install -g quicktype
# From file
quicktype --lang typescript --out types.ts < api-response.json
# From stdin
echo '{"id":1,"name":"Alice","email":null}' | quicktype --lang typescript
# Multiple samples (better nullable inference)
quicktype --lang typescript --out User.ts user1.json user2.json user3.json
# With runtime validator (generates both interface + Ajv validation)
quicktype --lang typescript --just-types=false --out User.ts < user.jsonGenerated output for {"{"}"id":1,"name":"Alice","email":null,"tags":["dev"]{"}"}:
export interface User {
id: number
name: string
email: string | null
tags: string[]
}3. Manual Conversion — Step by Step
// Source JSON (API response)
{
"id": 1,
"name": "Alice Smith",
"email": "alice@example.com",
"bio": null,
"role": "admin",
"createdAt": "2026-01-15T10:30:00Z",
"address": {
"street": "123 Main St",
"city": "Portland",
"country": "US"
},
"orders": [
{ "id": 101, "total": 49.99, "status": "shipped" }
]
}
// TypeScript interfaces
interface Address {
street: string
city: string
country: string
}
interface Order {
id: number
total: number
status: 'pending' | 'shipped' | 'delivered' | 'cancelled'
}
interface User {
id: number
name: string
email: string
bio: string | null // null in JSON → string | null
role: 'admin' | 'user' // enum-like values → union literals
createdAt: string // ISO string → string (or Date if parsed)
address: Address // nested object → separate interface
orders: Order[] // array of objects → T[]
}4. Handling Common Patterns
// Optional nested objects (may be absent)
interface UserProfile {
id: number
avatar?: { // optional block
url: string
width: number
height: number
}
}
// Union types for polymorphic arrays
type Notification =
| { type: 'email'; recipient: string; subject: string }
| { type: 'sms'; phone: string; message: string }
| { type: 'push'; deviceToken: string; title: string }
// Paginated API response (reusable generic)
interface PaginatedResponse<T> {
data: T[]
total: number
page: number
perPage: number
hasMore: boolean
}
// Use: const response: PaginatedResponse<User>
// Record for dynamic keys (JSON objects with variable property names)
interface CountByStatus {
[status: string]: number // index signature
}
// or: type CountByStatus = Record<string, number>
// as const for literal types (no inference widening)
const ORDER_STATUSES = ['pending', 'shipped', 'delivered'] as const
type OrderStatus = (typeof ORDER_STATUSES)[number] // 'pending' | 'shipped' | 'delivered'5. Zod — Runtime Safety + Types in One
import { z } from 'zod'
// Define once — get both TypeScript type and runtime validator
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
bio: z.string().nullable(),
role: z.enum(['admin', 'user']),
createdAt: z.string().datetime(),
address: z.object({
street: z.string(),
city: z.string(),
country: z.string().length(2),
}),
orders: z.array(z.object({
id: z.number(),
total: z.number().positive(),
status: z.enum(['pending', 'shipped', 'delivered', 'cancelled']),
})),
})
// Inferred TypeScript type — same as writing the interface manually
type User = z.infer<typeof UserSchema>
// Runtime validation of API response
const result = UserSchema.safeParse(apiResponse)
if (result.success) {
const user = result.data // typed as User
} else {
console.error(result.error.issues)
}6. From OpenAPI Spec (Best for API Clients)
# Install openapi-generator-cli
npm install -g @openapitools/openapi-generator-cli
# Generate TypeScript-fetch client from Swagger/OpenAPI spec
openapi-generator-cli generate \
-i https://api.example.com/openapi.json \
-g typescript-fetch \
-o ./src/api-client
# Generated files include typed interfaces for all schemas:
# src/api-client/models/User.ts
# src/api-client/apis/UsersApi.ts
# Alternative: orval (faster, better tree-shaking)
npx orval --config orval.config.tsFrequently Asked Questions
What is the best tool to convert JSON to TypeScript interfaces?
quicktype is the most capable tool for JSON to TypeScript conversion. It can process a single JSON example or multiple examples (for better inference), infer nullable vs optional fields from multiple samples, generate runtime validators alongside types, and produce enums instead of string literals. Install: npm install -g quicktype. Usage: quicktype --lang typescript --out User.ts < user.json or echo '{"id":1,"name":"Alice"}' | quicktype --lang typescript. For online use, app.quicktype.io lets you paste JSON and download the TypeScript file. For simpler cases, json-to-ts (npm install -g json-to-ts) generates interfaces without runtime code and is faster for quick one-off conversions. For generating types from an OpenAPI spec, openapi-generator or the official @openapitools/openapi-generator-cli generate typed interfaces with proper nullable handling from the spec's schema.
How do I manually write a TypeScript interface from a JSON object?
Map each JSON type to its TypeScript counterpart: JSON string → string, JSON number → number, JSON boolean → boolean, JSON null → null (combine with | for nullable: string | null), JSON array → T[] where T is the element type, JSON object → interface or inline type. Nullable field: "middleName": null in the JSON → middleName: string | null (if it can also be a string). Optional field: a field that does not appear in all examples → property?: string. The pattern: for each JSON object, create an interface where each key becomes a property, the type is inferred from the value, and ? is added for fields that are not consistently present across all samples. For deeply nested JSON, create one interface per distinct object shape and reference them by name. For arrays of heterogeneous types, use a union type or discriminated union.
How does quicktype handle nullable and optional fields?
quicktype infers nullable and optional fields from the provided samples. If you provide one JSON sample where "bio" is null and another where it is a string, quicktype generates bio: string | null. If "email" is absent in some samples, it generates email?: string. The more samples you provide, the more accurate the inference. To provide multiple samples to quicktype, use the --additional-schema flag or pipe an array of examples. For single-example inference, quicktype uses heuristics — a null value alone produces null type which quicktype often upgrades to string | null for practicality. The --infer-maps flag tells quicktype to prefer { [key: string]: T } for objects with many numeric/dynamic keys rather than generating interface properties. The --prefer-types flag produces type aliases instead of interfaces.
How do I convert a JSON API response type to a TypeScript interface in VS Code?
The "Paste JSON as Code" VS Code extension (by quicktype) converts clipboard JSON to TypeScript with one command. Install it, then: copy your JSON response to the clipboard, open a .ts file, press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows), type "Paste JSON as Code", and the extension generates typed interfaces at the cursor. Alternatively, use the TypeScript JSON-to-Types snippet approach: type Response = typeof sampleResponse. Write const sampleResponse = <your-json-here> as const, then derive the type with type Response = typeof sampleResponse. This approach gives exact literal types (useful for discriminated unions) rather than general types (number instead of 42, string instead of "active"). For API integration workflows, consider using the OpenAPI TypeScript generator from the API's Swagger spec — this stays in sync when the API changes.
How do I handle arrays of mixed types in TypeScript from JSON?
JSON arrays with mixed types are unusual but occur in practice. For example: [1, "two", null, true]. The TypeScript type is (number | string | null | boolean)[]. For structured mixed arrays (tuples), use tuple types: type Pair = [string, number] for ["Alice", 30]. For arrays of polymorphic objects — objects with different shapes identified by a type discriminator — use discriminated unions: type Shape = | { type: "circle"; radius: number } | { type: "rect"; width: number; height: number }. An array of shapes is Shape[]. When using quicktype on JSON with a consistent type discriminator, it can infer the union automatically. For runtime safety with mixed arrays, validate with Zod: z.array(z.union([z.number(), z.string(), z.null(), z.boolean()])) or z.discriminatedUnion("type", [CircleSchema, RectSchema]).
Is a TypeScript interface the same as a JSON Schema?
No — they serve different purposes. TypeScript interfaces are compile-time constructs. They are erased by the TypeScript compiler and produce no runtime code. They cannot validate data at runtime. JSON Schema is a runtime document (a JSON object that describes the shape of another JSON object). It can be compiled into a validator function (Ajv, jsonschema) to check real data at runtime. TypeScript interfaces give you IDE autocomplete and type errors during development. JSON Schema gives you runtime validation of API input, user uploads, or external data. You need BOTH for a complete solution: TypeScript interfaces for development-time safety, JSON Schema (or Zod, which generates both) for runtime safety. Tools to convert between them: zod-to-json-schema converts Zod schemas to JSON Schema; quicktype can generate both a TypeScript interface and a runtime validator (JSON Schema or Ajv-based) from sample JSON in one pass.
How do I type a JSON API response that can return different shapes?
Use a discriminated union. If the API returns different object shapes depending on a field value: type ApiResponse<T> = | { success: true; data: T } | { success: false; error: string; code: number }. The caller uses a type guard: if (response.success) { /* response.data is typed as T */ } else { /* response.error is typed as string */ }. For paginated responses: type PaginatedResponse<T> = { items: T[]; total: number; page: number; hasMore: boolean }. For the standard JSON:API format: type JsonApiResponse<T> = { data: T | T[] | null; errors?: JsonApiError[]; meta?: Record<string, unknown> }. Use TypeScript generics to make the response type reusable across different resource types. In React Query or SWR, these generic response types integrate with useQuery<ResponseType>() for fully typed data and error states.
What are the TypeScript equivalents of JSON Schema types?
JSON Schema type to TypeScript type mapping: "string" → string; "number" → number; "integer" → number (TypeScript has no separate integer type); "boolean" → boolean; "null" → null; "array" → T[] or Array<T>; "object" → interface or Record<string, T>; type array ["string", "null"] → string | null; enum with strings → union of string literals: "red" | "green" | "blue" or a TypeScript enum. For more complex mappings: anyOf/oneOf with objects → discriminated union or plain union. allOf → intersection type: TypeA & TypeB. not → no direct TypeScript equivalent (use unknown and narrow at runtime). $ref → reference another interface by name. required array determines which properties are required (no ?) vs optional (?). additionalProperties: false → use a specific interface (no index signature). additionalProperties: {type: string} → { [key: string]: string } index signature or Record<string, string>.
Further reading and primary sources
- quicktype — JSON to TypeScript — Online quicktype — paste JSON, get TypeScript interfaces instantly
- JSON to TypeScript (Jsonic tool) — Convert JSON to TypeScript interfaces in the browser — no install required
- Parse JSON in TypeScript (Jsonic) — Runtime-safe JSON parsing in TypeScript with Zod and type guards
- Zod Schema Validation (Jsonic) — Define schema once, get TypeScript types and runtime validation from one source
- JSON Schema vs TypeScript (Jsonic) — When to use JSON Schema vs TypeScript interfaces — compile-time vs runtime validation