Command Palette

Search for a command to run...

How to Generate TypeScript Types From JSON

How to Generate TypeScript Types From JSON

T
Toolz Team
|Jul 21, 2026|24 min read

Part of the Data Tools collection

JSON to TypeScript

Generate TypeScript interfaces from any JSON sample. Infers nested types, merges arrays of objects into a single interface, marks nullable and missing keys optional, and emits clean exportable code. 100% client-side.

Use the JSON to TypeScript

The bug that finally made me stop hand-writing API types was embarrassingly small. A payments endpoint returned discount: null for customers without one, and I had typed it discount: number because the one response I looked at while writing the interface happened to be a customer with a discount. TypeScript was perfectly happy. The compiler had no way to know I'd lied to it. Three weeks later a .toFixed(2) on that field threw in production for exactly the subset of users who mattered least to my testing and most to the invoice.

That is the whole problem with typing an API by hand: you type what you believe the endpoint returns, and the compiler dutifully enforces your belief rather than reality. Every guarantee TypeScript gives you downstream is only as good as that first hand-written interface, and there is nothing in the toolchain that checks it against an actual response. You get all the ceremony of static typing with none of the safety, which is arguably worse than no types at all - at least untyped code makes you suspicious.

Generating types from a real payload flips the direction. Instead of describing what you think the shape is, you take a response the server actually sent and derive the shape from it. The output is mechanical: no optimism, no fields you forgot existed, no number where the data says number | null. I build toolz.dev and put a browser-based JSON to TypeScript converter there that does this, but this guide is about the inference rules themselves - what a generator can figure out, what it can only guess at, and where you still have to think.

TL;DR: To convert JSON to TypeScript, infer each key's type from its value (string, number, boolean, null), extract nested objects into their own named interfaces, and merge arrays of objects into a single element interface where any key missing from some members becomes optional. Decide deliberately whether null means key?: T or key: T | null- that choice depends on whether your API omits absent fields or sends them as null. Inference reflects only the sample you provide, so use a representative payload with several records, and treat the output as a reviewed first draft rather than a finished contract.

Why generate TypeScript types from JSON instead of writing them?

The honest answer is that hand-written types drift and generated types don't. When the backend adds a field, your hand-written interface silently stays wrong; nothing errors, because extra properties in a response are invisible to a type that doesn't mention them. When the backend changes id from a number to a string, your interface keeps insisting it's a number and TypeScript keeps agreeing, right up until something concatenates instead of adds.

There's also the plain tedium argument. A typical REST response has thirty keys across four levels of nesting. Transcribing that by hand takes ten minutes of pure mechanical work, and mechanical work performed by humans has a defect rate. You will typo a key name. You will miss the one field that's an array of objects rather than an array of strings. The generator will not.

But the strongest reason is that generation makes the shape visible. Paste a response into a converter and you immediately see things you'd have glossed over reading raw JSON: that metadata is actually a deeply nested object, that tags is sometimes empty, that half the keys in your paginated list are missing from some records. The generated interface is a summary of the data's real structure, and reading it is often the fastest way to understand an endpoint you didn't write. I've used it as a documentation step more than once on APIs whose docs were a lie.

Where this fits alongside your other data tools: if you're inspecting the payload rather than typing it, the JSON Formatter is the better first stop, and if you're comparing two responses to see what changed between versions, the JSON Diff answers that directly.

How does type inference from JSON actually work?

JSON has six value types, per RFC 8259: object, array, string, number, true/false, and null. TypeScript's primitive types map onto four of those almost directly. The interesting work is entirely in the other two.

Primitives are trivial. A string value implies string. A number implies number- note that JSON has one numeric type, so there's no information in the data telling you whether 1 is an integer or a float, and TypeScript doesn't distinguish anyway. true or false implies boolean. This part has no ambiguity.

Objects become interfaces. Every object value becomes a named interface, and the key it appeared under supplies the name, converted to PascalCase. A key owner produces interface Owner. Nesting recurses: an object inside an object produces a second interface referenced from the first. This matters more than it sounds. The alternative - inlining every nested shape anonymously - produces a single unreadable declaration and gives you nothing importable:

// Inlined: technically correct, practically useless
interface Project {
  owner: { id: number; email: string; twoFactor: boolean }
}

// Extracted: you can import and reference Owner on its own
interface Project {
  owner: Owner
}

interface Owner {
  id: number
  email: string
  twoFactor: boolean
}

Once Owner exists as a name, a function that takes just the owner can be typed (owner: Owner) => void. With the inlined version you'd be writing Project['owner'] everywhere, which works but reads badly.

Arrays are where the real decisions live. An array's type is the union of its element types, so [1, 2, 3] gives number[] and [1, "a"] gives (number | string)[]. Note the parentheses in that second one - without them, number | string[] means something entirely different (a number or an array of strings), and generators that forget this emit code that compiles but describes the wrong thing.

Empty arrays are an honest dead end. "tags": [] tells you a key exists and holds an array; it tells you nothing about what goes in it. The correct output is unknown[], and you should read that as the generator declining to guess rather than as a finished answer. Fill it in yourself from documentation, or find a sample where the array isn't empty.

Why are arrays of objects merged instead of unioned?

This is the single decision that separates a generator you'd use from one you'd abandon after five minutes.

Consider a paginated response where records aren't perfectly uniform - which is to say, every real paginated response:

{
  "rows": [
    { "id": 1, "name": "Ada", "nickname": "The Countess" },
    { "id": 2, "name": "Grace" }
  ]
}

Treat each element independently and you get a union of two interfaces: rows: (Row1 | Row2)[]. This is technically the most accurate reading of the sample, and it is useless. Every access to row.nickname now requires narrowing, because TypeScript can't know which member of the union you have. Extend that to a fifty-record response with several optional fields and you get a union of dozens of near-identical interfaces. Nobody wants that.

The useful reading is that these two objects are two instances of one entity, and nickname is a field Grace doesn't have:

interface Row {
  id: number
  name: string
  nickname?: string
}

interface T {
  rows: Row[]
}

That's a merge: collect every key seen across all elements, and mark a key optional if it's absent from any of them. It matches how the data is actually produced - one database table, one serialiser, some nullable columns - and it produces types you can use without ceremony. The array element name is also singularised, so releases yields Release rather than Releases, because releases: Releases[] reads like a bug even when it isn't.

The tradeoff is real and worth stating plainly: merging assumes the array is homogeneous. If you have a genuinely heterogeneous array - a feed of events with different shapes, discriminated by a type field - merging flattens distinct variants into one interface where nearly everything is optional. That's the wrong model, and it's a case where you should take the generated output as a starting point and hand-write a proper discriminated union. Generators don't know your domain. This one merges objects and unions everything else, which is right most of the time and wrong in a way you can spot immediately.

Should null become an optional key or a union member?

Both conventions are defensible and the difference bites, so decide on purpose rather than accepting whatever your tool defaults to.

Given { "retiredAt": null }, there are two readings:

interface A { retiredAt?: string }      // the field may be absent
interface B { retiredAt: string | null } // the field is present and may be null

They are not interchangeable. In A, retiredAt is string | undefined and the key might not exist on the object at all. In B, the key always exists, and its value might be null. Under strictNullChecks- which the TypeScript Handbook recommends and which you should have on - both force you to handle the absent case, but they force different checks and they serialise differently. JSON.stringify omits undefined properties entirely and emits null for null ones, so the choice propagates all the way back out to the wire.

The right answer depends on the API's actual behaviour, which no generator can see from one sample:

Your API's behaviour Correct model Why
Omits the key when there's no value key?: T The key genuinely isn't there; optional is accurate
Always sends the key, null when empty key: T | null The key is always present; ? would wrongly permit absence
Inconsistent - sometimes omitted, sometimes null key?: T | null Both cases are real; model both
Sends null only on error responses Neither - model the error separately A nullable field is hiding a union of response shapes

That last row is the one worth pausing on. A field that goes null only in failure cases is a signal that the endpoint returns two different things wearing one shape, and the fix is a discriminated union on a status field, not a nullable property. Type generation surfaces this pattern; it doesn't solve it.

The converter defaults to key?: T because omitted-when-absent is the more common convention in the JSON APIs I've worked with, and because it composes better with the array merging described above (a key missing from some records and a key that's null in some records get modelled the same way). Turn the option off and null stays in the union instead. Neither is a trick; pick the one your API actually does.

What about keys that aren't valid TypeScript identifiers?

JSON object keys are arbitrary strings. TypeScript property names in a bare key: T position are not - they have to be valid identifiers. So "content-type", "2fa", "user.name", and "" are all legal JSON keys that cannot be written unquoted in an interface.

The fix is quoting, and it's not a workaround - quoted property names are ordinary TypeScript:

interface Headers {
  "content-type": string
  "2fa": boolean
  class: string
}

Those properties are accessed with bracket notation (headers["content-type"]), which is slightly more verbose but entirely type-safe. Note that class doesn't need quoting: reserved words are perfectly legal as property names, even though they're illegal as identifiers. The restriction only applies where TypeScript expects an identifier - which is why the same word does need handling when it becomes an interface name.

Interface names derived from such keys need more work than quoting. 2fa PascalCases to 2fa, which can't start an identifier, so it gets a prefix. Two different nested objects both under keys named owner would both want to be Owner, so the second becomes Owner2. These are unglamorous details, and they're exactly the details that decide whether generated output compiles or needs fifteen minutes of hand-repair before it does. The test I hold the converter to is simple: paste anything valid, and the output should compile under strict with no edits.

Interfaces or type aliases?

The generator emits either. The practical differences are narrow but real, and your codebase probably already has an opinion encoded in its lint config.

interface User {} supports declaration merging - declare the same interface name twice and TypeScript combines them. That's essential for augmenting types from libraries you don't control, and a footgun everywhere else, since two unrelated declarations of the same name silently merge instead of erroring. Interfaces also support extends, which produces slightly better error messages than intersection types when a constraint fails.

type User = {} can't merge, which is usually a feature, and it's required for anything that isn't an object shape: unions, tuples, mapped types, conditional types. A root that isn't a JSON object - an array of numbers, a bare string - can only be expressed as an alias, so type Nums = number[] is what you get regardless of the setting.

For generated API types I lean toward interface, mostly because the error messages are marginally better and because the merging risk is theoretical when every name lives in one generated file. But this is close to a coin flip, and consistency with the surrounding code matters more than the merits. If your ESLint config has @typescript-eslint/consistent-type-definitions set either way, match it and stop thinking about it.

How is this different from JSON Schema to TypeScript?

These solve genuinely different problems and it's worth being precise, because "JSON to TypeScript" and "JSON Schema to TypeScript" are one word apart and frequently confused.

JSON to TypeScript is inference from an example. Input: a value. The generator observes what's there and generalises. It cannot know whether a field is required, whether a string is constrained to an enum, whether a number has a minimum, or whether the one sample you pasted is representative. It's induction from a single observation, with everything that implies.

JSON Schema to TypeScript is translation from a declaration. Input: a JSON Schema document, which already states types, required arrays, enums, formats, and constraints. The generator isn't guessing - it's transliterating an existing contract into TypeScript syntax. required maps to non-optional properties; an enum maps to a string literal union; oneOf maps to a union type.

The rule follows directly: if a schema exists, use it. A JSON Schema, an OpenAPI spec, a .proto file, or a GraphQL schema is authoritative in a way that a sampled response never is. Inference is what you reach for when no schema exists - an undocumented internal endpoint, a third-party API whose docs are stale, a config file format that grew organically, a fixture you're writing tests against. Which, in fairness, describes a large fraction of the JSON any of us actually deals with.

There's a middle path worth mentioning: use inference to bootstrap, then maintain by hand. Generate the interface from a real response to get the shape and the field names right, then edit it - tighten a string to a literal union where you know the allowed values, fix an unknown[] the sample left empty, split a merged interface into a proper discriminated union. The generator does the mechanical 90% and you apply the domain knowledge it structurally cannot have.

Where does inference get it wrong?

A short, honest list. Every one of these is a limitation of the approach, not a bug in a particular tool, and knowing them is the difference between using generated types well and getting burned by them.

Single samples underdetermine the type. A field that's number in your sample might be null in 5% of records. A field that's present in all three records you pasted might be optional across the full dataset. Inference reports what it saw. Paste more records - ideally a real page of results rather than one hand-picked object - and the optionals get meaningfully more accurate.

Strings hide their real types. ISO timestamps, UUIDs, URLs, and email addresses are all just string to a JSON parser. "2026-07-16T09:00:00Z" is semantically a date; nothing in the data says so. If your codebase has a branded ISODateString type, you're substituting it by hand.

Numbers lose precision distinctions. JSON's single number type means an ID that's a 64-bit integer on the server arrives as a JavaScript number and may already have lost precision before your generator ever sees it - Number.MAX_SAFE_INTEGER is about 9×10¹⁵, and Twitter famously learned this the hard way. If your API sends large integers as strings, that's why, and the generated string is correct.

Literal values look like their general types. "status": "active" infers string, not "active" | "archived" | "pending". The narrower type is more useful and no sample can prove it. This is the most common hand-edit I make to generated output.

Empty containers say nothing. [] gives unknown[] and {} gives an empty interface. Both are the generator being honest.

None of this makes inference unsafe - it makes it a draft. The workflow that works is: generate, read the output carefully, fix the four or five things you know that the sample couldn't say, commit. That's still an order of magnitude faster and more accurate than transcribing thirty keys by hand, which is the actual alternative.

Does my JSON get uploaded anywhere?

No, and this is a category of tool where the question deserves a real answer rather than a badge.

Think about what's in the JSON you'd paste into a type generator. It's an API response, which means it plausibly contains a bearer token, a session identifier, a customer email, an internal user ID, a pricing tier, a webhook secret. That's not hypothetical - it's the modal case, because the whole point is that you grabbed a real response to type against.

Any server-side converter necessarily receives that payload. It may not log it, and it probably doesn't, but you're extending trust you don't have to extend, and depending on the data you may be creating a compliance problem for a task that has no business touching a network at all.

Type inference is pure computation over a parsed value. It needs no network, no account, no storage. The converter on toolz.dev is a few hundred lines of dependency-free TypeScript running in your tab; the payload is a JavaScript string in your browser's memory and it stays there. You can verify this the way you'd verify any such claim - open the network tab and hit Generate, or turn off your wifi and watch it keep working. This is the same principle behind every tool on the site, and I've written about why it matters more broadly in why browser-based tools beat server-side ones for sensitive data.

A worked example

Here's the sample the tool ships with, which is deliberately constructed to exercise every rule above:

{
  "id": 4821,
  "name": "Toolz",
  "isPublic": true,
  "retiredAt": null,
  "owner": {
    "id": 12,
    "email": "[email protected]",
    "twoFactor": false
  },
  "tags": ["developer", "privacy", "browser"],
  "releases": [
    { "version": "1.0.0", "downloads": 1420, "notes": "First cut" },
    { "version": "1.1.0", "downloads": 3310 }
  ]
}

With the root named Project, that generates:

export interface Project {
  id: number
  name: string
  isPublic: boolean
  retiredAt?: null
  owner: Owner
  tags: string[]
  releases: Release[]
}

export interface Owner {
  id: number
  email: string
  twoFactor: boolean
}

export interface Release {
  version: string
  downloads: number
  notes?: string
}

Read what happened. owner was extracted into its own interface and referenced by name. tags collapsed to string[] because every element was a string. releases merged its two members into one Release- singularised - and notes became optional because the second release didn't have one. retiredAt became optional because its only observed value was null.

And now read what you'd fix. retiredAt?: null is the generator's honest report that it has never seen a non-null value, and it's useless as a type - you'd change it to retiredAt?: string because you know it's a timestamp when present. That single edit is the whole lesson: the generator got the seven-key structure, the nesting, the array merge, and the optionality right in one paste, and left you with the one decision that required knowing what the field means.

FAQ

How do I convert JSON to a TypeScript interface?

Paste your JSON into the converter, set the root type name to whatever the resource is called, and press Generate. It infers the type of every key, pulls nested objects out into their own named interfaces, merges arrays of objects into a single element type, and outputs code you can copy straight into a .ts file. There's no signup and no upload - the inference runs in your browser.

What happens to arrays of objects?

They're merged into one interface describing a single element, and the property is typed as an array of it. Any key that appears in some array members but not others becomes optional. This matches how real paginated data behaves, where records come from one table and some columns are nullable. The one case it handles badly is a genuinely heterogeneous array of different event types, which you should hand-convert into a discriminated union.

Should null become an optional key or a union with null?

It depends on whether your API omits absent fields or sends them as null. If it omits them, key?: T is accurate. If the key is always present and sometimes null, key: T | null is accurate, and using ? would wrongly allow the key to be missing. The converter defaults to optional and lets you switch, because the two serialise differently - JSON.stringify drops undefined properties but emits null ones.

Can it infer accurate types from a single JSON sample?

It infers accurate types for that sample, which isn't the same thing. A field that's a number in your one record might be null in others; a field present in all three records you pasted might be optional across the full dataset. Use a representative payload with several records rather than one hand-picked object, and treat the output as a reviewed draft rather than a finished contract.

What's the difference between JSON to TypeScript and JSON Schema to TypeScript?

This tool infers types from an example value; JSON Schema to TypeScript translates a formal schema that already declares types, required fields, and enums. The schema is authoritative and inference is a guess, so if you have a JSON Schema, an OpenAPI spec, or a GraphQL schema, use it. Inference is for the very common case where no schema exists and all you have is a response body.

How does it handle keys that aren't valid identifiers?

Keys with dashes, dots, spaces, or leading digits are quoted in the output, so "content-type" becomes "content-type": string. That's valid TypeScript, accessed with bracket notation. Reserved words like class don't need quoting as property names. Interface names derived from such keys are PascalCased and prefixed if they'd start with a digit, and colliding names get a numeric suffix so the output always compiles.

Should I generate interfaces or type aliases?

Match whatever your codebase already does - this is mostly a consistency question. Interfaces support declaration merging and extends, and give marginally clearer error messages. Type aliases can't merge, which is usually desirable, and are required for anything that isn't an object shape. A root that's an array or a primitive is emitted as an alias either way, since there's no object to declare an interface for.

Is my JSON uploaded to a server?

No. The entire inference engine runs as JavaScript in your browser, with no network calls, no logging, and no storage. This matters here more than for most tools, because the JSON you'd paste into a type generator is usually a real API response containing tokens, customer records, or internal IDs. Open your network tab while generating, or disconnect from the internet - it keeps working.


Related tools: JSON Formatter for inspecting the payload first, JSON to YAML and JSON to XML for format conversion, and JSON Diff for spotting what changed between two responses. Further reading: the ultimate guide to JSON tools and the developer's coding tools guide.

Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!