Turning JSON into TypeScript interfaces is easy when the sample is small. Paste a response, click convert, and you get a tidy interface.
The hard part starts five minutes later: the next response is missing a field, middleName is sometimes null, status should be an enum instead of string, an empty array became unknown[], and response.json() still accepts anything at runtime.
This guide treats JSON-to-TypeScript conversion as an API integration workflow, not just a code-generation trick. The goal is to generate a useful first draft, then tighten it so it matches the contract your app actually depends on.
Quick Decision Table
| Situation | Best approach | Why |
|---|---|---|
| One sample from a new endpoint | Use JSON to TypeScript | Fast first draft, browser-local, good for exploration. |
| Several sample responses | Merge samples before generating | Captures optional fields and nullable variants better than one example. |
| Long-lived external API | Generate from OpenAPI with openapi-typescript |
The spec describes more cases than a sample response can show. |
| Unknown or user-provided JSON | Generate types plus runtime validation | Interfaces disappear at runtime; validate with Zod or JSON Schema. |
| LLM output or broken pasted JSON | Repair and validate JSON first | Type generation needs a parsed JSON value. |
| Internal API you control | Prefer shared schema or OpenAPI source of truth | Keeps server, client, tests, and docs in sync. |
Use a sample-based converter to move quickly. Use a schema or API spec when correctness and long-term maintenance matter.
What a Converter Can Infer From JSON
Start with a realistic response:
{
"id": "u_123",
"email": "ada@example.com",
"displayName": "Ada Lovelace",
"middleName": null,
"createdAt": "2026-05-24T15:30:00.000Z",
"plan": "team",
"roles": ["admin", "editor"],
"profile": {
"timezone": "America/Los_Angeles",
"marketingEmail": false
},
"projects": [
{
"id": "p_1",
"name": "API cleanup",
"archived": false
}
]
}
A useful first draft looks like this:
interface UserResponse {
id: string;
email: string;
displayName: string;
middleName: null;
createdAt: string;
plan: string;
roles: string[];
profile: Profile;
projects: Project[];
}
interface Profile {
timezone: string;
marketingEmail: boolean;
}
interface Project {
id: string;
name: string;
archived: boolean;
}
That is better than any, but it is not finished. A generator can only infer what the sample proves. It cannot know that:
middleNameshould bestring | nullplanshould be'free' | 'team' | 'enterprise'createdAtis an ISO date-time stringprojectsmay be emptyemailshould pass email validation- some fields are server-owned and should not appear in request bodies
Generation gives you the scaffold. Review turns it into a contract.
JSON to TypeScript Mapping Rules
| JSON sample value | First-pass TypeScript type | Review note |
|---|---|---|
"Ada" |
string |
May need a literal union for known states. |
42, 3.14 |
number |
TypeScript has no integer type. |
true |
boolean |
Usually safe. |
null |
null |
Often too narrow from a single sample. |
{ "id": "u_1" } |
named interface | Naming needs review. |
["admin", "editor"] |
string[] |
Could be Role[] if values are known. |
[1, "two"] |
`(number | string)[]` |
[] |
unknown[] |
Needs schema knowledge or more samples. |
"2026-05-24T15:30:00.000Z" |
string |
JSON has no Date type. |
"12345678901234567890" |
string |
Good for IDs and big integers. |
The generator should be conservative. If it guesses too specifically from one sample, your next API response breaks the type.
Do Not Treat response.json() as Typed
This pattern is common but misleading:
interface User {
id: string;
email: string;
}
const response = await fetch('/api/users/u_123');
const user: User = await response.json();
TypeScript trusts the annotation, but no runtime check happened. If the server returns { "id": 123 }, TypeScript will not stop it. The value entered your program as untrusted data and was simply labeled User.
For an internal endpoint you fully control, that may be an acceptable tradeoff. For external APIs, user uploads, webhook bodies, LLM output, or anything security-sensitive, validate at the boundary.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const response = await fetch('/api/users/u_123');
const raw = await response.json();
const user = UserSchema.parse(raw);
Zod gives you both a runtime parser and an inferred TypeScript type. The official Zod docs describe it as a TypeScript-first validation library with static type inference, which is exactly the missing piece after interface generation.
Method 1: Use a Browser Converter for a First Draft
For a one-off integration, paste a response into JSON to TypeScript. It runs locally in the browser and is fastest when you are still exploring the shape of a payload.
Use this path when:
- you have one valid JSON response
- you need a quick type scaffold
- the data is sensitive and should not be uploaded
- you plan to review the output before committing it
Before converting, make sure the input is valid JSON. If the sample has single quotes, trailing commas, comments, or markdown fences, repair it first with JSON Fix, then convert the cleaned result.
After converting, immediately rename the root type. Root is fine for a tool output pane; UserResponse, InvoiceListResponse, or CreateWebhookPayload is better in a codebase.
Method 2: Use Multiple Samples
One JSON sample lies by omission. It can show a field that exists, but it cannot show every field that might be missing.
Sample A:
{
"id": "ord_1",
"total": 49.99,
"discount": 5,
"note": null
}
Sample B:
{
"id": "ord_2",
"total": 19.99,
"note": "Gift wrap"
}
The reviewed TypeScript should be:
interface Order {
id: string;
total: number;
discount?: number;
note: string | null;
}
discount?: number means the key may be absent. note: string | null means the key is expected but the value may be null.
A good generator can infer this only if it sees both variants. If you paste one happy-path sample, it will understate the contract.
Method 3: Use quicktype for CLI or Batch Work
For repeatable generation from files, URLs, JSON Schema, or a directory of samples, use quicktype. Its project page describes input support for sample JSON files, URLs, JSON Schema, and GraphQL queries, and it can generate TypeScript plus runtime checks.
npm install -g quicktype
quicktype api-response.json -o types/user.ts --lang typescript
quicktype https://api.example.com/users/u_123 -o types/user.ts --lang typescript
cat api-response.json | quicktype --lang typescript
Use quicktype when:
- you need repeatable generation
- you have multiple sample files
- the output should be regenerated in a script
- you want types in several languages, not just TypeScript
Be careful with generation from live URLs in CI. A transient API response can change your generated code unexpectedly. Prefer pinned sample fixtures or an API schema for automated builds.
Method 4: Generate From OpenAPI for Production APIs
If an API has an OpenAPI spec, generate from the spec rather than from a sample response.
npm install -D openapi-typescript
npx openapi-typescript ./openapi.json -o src/types/api.ts
The OpenAPI TypeScript project describes its goal as converting OpenAPI 3.0 and 3.1 schemas to TypeScript types, with zero runtime cost for the generated static types. That is the right model for long-lived API clients: the contract lives in the schema, and clients regenerate from it.
Use this path when:
- the API has many endpoints
- optional and nullable behavior matters
- request and response types both need coverage
- enum values should come from the spec
- CI should catch contract changes
For typed requests, pair generated paths types with a client such as openapi-fetch, or use your existing fetch wrapper with the generated request and response types.
Interface, Type Alias, or Schema?
Generated output often uses interface, but TypeScript gives you a few shapes.
Use interface for object shapes that may be extended:
interface User {
id: string;
email: string;
}
Use type for unions, primitives, mapped types, and aliases:
type Plan = 'free' | 'team' | 'enterprise';
type UserId = string;
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };
Use a runtime schema when the data crosses a trust boundary:
const PlanSchema = z.enum(['free', 'team', 'enterprise']);
type Plan = z.infer<typeof PlanSchema>;
A mature codebase often uses all three: interfaces for object responses, type aliases for unions and IDs, and schemas for data that must be validated at runtime.
Tricky Cases to Review Manually
Optional vs nullable
Optional means the key may be absent:
interface User {
phone?: string;
}
Nullable means the key is present but its value may be null:
interface User {
phone: string | null;
}
Those are not interchangeable. They affect form defaults, PATCH requests, database updates, and UI copy.
Empty arrays
An empty array proves almost nothing:
{ "items": [] }
unknown[] is safer than pretending the array is string[]. Use another sample, documentation, or a schema to decide the element type.
Mixed arrays
{ "values": [1, "2", null] }
The honest type is:
interface Root {
values: Array<number | string | null>;
}
That may be correct for a flexible search filter, but it is often a warning that the sample combines different shapes accidentally.
Enums and literal unions
A single sample can only show "pending". It cannot know the complete set.
type OrderStatus = 'pending' | 'paid' | 'failed' | 'refunded';
interface Order {
status: OrderStatus;
}
Use docs, OpenAPI, JSON Schema, or production-safe samples to fill in the union. Do not invent values from hope.
Dates
JSON has strings, not dates:
interface User {
createdAt: string;
}
If your app parses the string into a Date, model that as a separate transformed type:
interface UserResponse {
createdAt: string;
}
interface UserModel {
createdAt: Date;
}
Keeping wire types separate from app models prevents a lot of quiet bugs.
IDs and large numbers
JSON numbers become JavaScript numbers, and JavaScript numbers cannot safely represent every large integer. If an ID can exceed the safe integer range, keep it as a string:
interface Payment {
id: string;
amountCents: number;
}
For exact decimal money values, consider strings or a decimal library rather than plain number.
Keys that are not valid identifiers
JSON keys can contain hyphens, spaces, and dots:
{
"content-type": "application/json",
"x-request-id": "req_123"
}
TypeScript can represent them with quoted property names:
interface HeadersLike {
"content-type": string;
"x-request-id": string;
}
Access those keys with bracket notation:
headers["content-type"];
Additional fields
Generated interfaces usually list known keys only. APIs often add fields later.
For strict internal code, ignore unknown fields. For pass-through proxies or config objects, you may need an index signature:
interface FeatureFlags {
search: boolean;
billing: boolean;
[key: string]: boolean;
}
Use index signatures sparingly. They make typo detection weaker.
A Safer Fetch Pattern
If you are not ready for full runtime schemas, at least isolate the unsafe cast in one place.
interface UserResponse {
id: string;
email: string;
}
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<T>;
}
const user = await fetchJson<UserResponse>('/api/users/u_123');
This still does not validate the data, but it keeps the boundary visible. When the endpoint becomes important, replace the cast with schema parsing:
async function fetchParsed<T>(url: string, schema: z.ZodType<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return schema.parse(await response.json());
}
Keep Generated Types in Sync
Generated types rot when nobody owns regeneration.
For sample-based generation:
- keep representative fixtures in the repo
- include success, empty, nullable, and error examples
- regenerate types with a script
- run
tsc --noEmitafter regeneration - diff generated output in pull requests
For OpenAPI-based generation:
- pin the schema URL or commit the schema file
- regenerate in CI
- fail the build when generated files drift
- review breaking type changes before release
Example package scripts:
{
"scripts": {
"types:api": "openapi-typescript ./openapi.json -o src/types/api.ts",
"check": "npm run types:api && tsc --noEmit"
}
}
If generated files change on every run, fix the generator settings before trusting the diff.
Use JSON Fix Before Type Generation
Type generators need parsed JSON. If the input has single quotes, comments, trailing commas, Python literals, or an LLM markdown fence, repair it first:
broken JSON -> repair -> validate -> generate TypeScript -> review types
That is why the JSON to TypeScript tool pairs naturally with JSON Fix and JSON Validator. Syntax repair is only step one; interface review is where you decide optional, nullable, enum, date, ID, and validation behavior.
Frequently Asked Questions
How do I convert JSON to a TypeScript interface?
Paste a valid JSON sample into JSON to TypeScript for a quick draft, use quicktype for CLI generation, or generate from an OpenAPI spec with openapi-typescript for long-lived API code.
Should I generate interfaces from a sample response or OpenAPI?
Use a sample response for exploration and prototypes. Use OpenAPI or JSON Schema when the API is long-lived, because a schema can describe optional fields, nullable values, enums, request bodies, and error responses that one sample cannot show.
Do TypeScript interfaces validate JSON at runtime?
No. Interfaces are erased at compile time. response.json() returns untrusted runtime data, so validate external data with Zod, JSON Schema, or another runtime parser when correctness matters.
How do I handle optional vs nullable fields?
Use field?: T when the key may be absent. Use field: T | null when the key is present but the value may be null. If both can happen, use field?: T | null.
What type should an empty JSON array become?
Usually unknown[] until you have another sample or a schema. An empty array does not prove whether future items are strings, objects, numbers, or a union.
Should date strings become Date in the interface?
Usually no. JSON carries date-times as strings. Model the API response as createdAt: string, then convert to Date in a separate application model if your code needs Date methods.
Should I use interface or type for generated output?
Use interfaces for object shapes. Use type aliases for unions, literals, branded IDs, generic results, and primitive aliases. Many real projects use both.
Is it safe to paste JSON into an online TypeScript converter?
Only if the converter runs locally in your browser or the JSON is already safe to share. API responses often contain tokens, emails, customer records, or internal configuration, so avoid server-side converters for sensitive data.
Related Tools & Guides
- JSON to TypeScript - generate interfaces locally in your browser.
- JSON Fix - repair broken JSON before converting it to types.
- How to Validate JSON - syntax and schema validation workflow.
- What Is JSON Schema? - describe data shape beyond one sample.
- JSON Format Examples - review the six JSON data types before mapping them to TypeScript.
- JSON vs JavaScript Object Literal - avoid converting JavaScript-looking data as if it were JSON.
Sources
- quicktype - generates types and code from JSON, schemas, URLs, and other inputs.
- Zod - TypeScript-first schema validation with static type inference.
- OpenAPI TypeScript - converts OpenAPI 3.0 and 3.1 schemas to TypeScript types.
- openapi-fetch - type-safe fetch client for OpenAPI-generated types.
- TypeScript Handbook: Everyday Types - interfaces, arrays, unions, and object types.
Last reviewed July 2026.