JSON to TypeScript Converter

Paste JSON to generate TypeScript interfaces instantly. 100% client-side.

Last reviewed: April 2026

New to this tool? Click here for instructions

JSON Input
TypeScript Output
Paste JSON above to generate TypeScript interfaces.

Paste any JSON payload and instantly generate matching TypeScript interfaces, type aliases, or readonly types — with nested objects split into named types, arrays inferred to their element type, and null values surfaced as union members. Every conversion runs entirely in your browser. 🔒

What This Tool Does

This tool consumes a JSON document and emits a set of TypeScript type declarations whose structural shape matches the input exactly. Every primitive value is mapped to its corresponding TypeScript primitive ("hello"string, 42number, trueboolean, nullnull). Every nested object becomes its own named type — typically Pascal-cased after its parent key — so the generated code stays composable: each interface can be imported, extended, intersected, or used as a function parameter type on its own.

Arrays of primitives are inferred to T[] form where T is the element type (["a", "b"]string[]). Arrays of objects produce a separately named element interface plus an array-of-element reference in the parent (users: User[], with User defined in its own block). Where the JSON value is null, the field type becomes the bare null literal — you typically widen this to T | null after generation if the field is actually optional or nullable rather than always-null.

Three output modes are available. Interface mode (default) emits interface User { id: number; ... } declarations, which support declaration merging and the extends keyword — the right choice for shapes that may be augmented downstream. Type alias mode emits type User = { id: number; ... };, which composes cleanly with unions and intersections. Readonly mode prefixes every property with the readonly modifier, producing immutable shapes appropriate for modeling API responses that should never be mutated after receipt. No JSON you paste is uploaded, logged, or transmitted anywhere outside your local browser session.

How to Use It: Paste, Pick Mode, Copy

The interface is deliberately compact — three steps from paste to usable type definitions, with no signup, no upload, and no rate limit.

1. Paste Your JSON

Drop a JSON document into the left-hand pane. The parser accepts anything JSON.parse() would accept: objects, arrays of objects, deeply nested structures, mixed primitive types, and null values. The textarea supports Tab indentation (Tab inserts two spaces rather than shifting focus), and a debounced parse fires 150 ms after the last keystroke, so the generated TypeScript updates as you type without overwhelming the parser on each character. Click Try Example to pre-load a representative payload with nested objects, arrays, and a null field — useful for confirming the tool is wired up before pasting real production data.

2. Pick a Generator Mode

Three mode chips sit above the input pane. Interface emits interface Name { ... } declarations — the default and the right choice for most projects. Type Alias emits type Name = { ... }; declarations, which integrate more naturally with union types, intersections, and computed types. Readonly emits interface-form declarations with every property prefixed by readonly, freezing the shape against mutation in TypeScript's structural type system. Switching modes re-renders the output immediately — your input stays in place, only the format changes.

3. Copy or Download

The output pane displays the generated TypeScript with child types ordered first and the root type last, so a paste into a fresh .ts file compiles in order without forward-reference warnings. Copy writes the full output to your clipboard. Download saves a output.ts file. The status bar below the panes reports how many types were generated and whether parsing succeeded — when JSON parsing fails, the bar turns red with the parser's exact error message, including line and column when available.

Tab and Keyboard Behavior

Pressing Tab inside the input textarea inserts two spaces rather than moving focus to the next element. This matches the indentation behavior most developers expect when pasting and reformatting JSON inline. To advance focus, use the standard Shift-Tab combination or click elsewhere on the page.

Worked Example: Real API Response → TypeScript Types

The example below walks through a realistic API payload — the kind of response a typical user-profile endpoint would return — and shows exactly how the tool maps each JSON value into its TypeScript equivalent. Every inference choice is traced from input to output so you can verify the tool's behavior matches expectations before relying on it for production code.

Input JSON

{
  "user": {
    "id": 42,
    "email": "a@b.com",
    "tags": ["admin", "user"],
    "created": "2025-01-15T10:30:00Z",
    "profile": null
  }
}

Generated TypeScript (Interface Mode)

interface Profile {
}

interface User {
  id: number;
  email: string;
  tags: string[];
  created: string;
  profile: null;
}

interface RootObject {
  user: User;
}

Inference Walk-Through

  1. Root object detection. The top-level value is an object, so the tool emits interface RootObject { ... }. Its single property user points at another object, which triggers recursion into a named child type. The parent property's type becomes the child interface name: user: User.
  2. Primitive inference. Inside User, id: 42 maps to number (TypeScript has no separate integer type — all JSON numbers are number). email: "a@b.com" maps to string regardless of email-pattern content; the tool does not infer branded types or template literal types from value shape. tags: ["admin", "user"] is an array of strings, so the element type is string and the field becomes string[].
  3. Date-like strings stay as string. The created value "2025-01-15T10:30:00Z" looks like an ISO 8601 timestamp, but JSON has no date type. The tool emits created: string, which is technically correct — at the JSON wire level, that value is a string. If you want a Date object in your application, parse it after deserialization rather than declaring it as Date in the type (the type would lie, since JSON.parse() returns a string).
  4. Null inference. profile: null emits the literal type null. This is rarely what you want long-term — null alone means "this field is always null," which is almost never accurate. Hand-widen the field to Profile | null after generation if profile can sometimes hold an object. The tool also emits an empty interface Profile {} placeholder so the named type exists for reference; flesh it out from a non-null sample later.
  5. Type ordering. Child types appear before their parents in the output (Profile first, then User, then RootObject). This guarantees the generated file compiles cleanly when pasted into a fresh .ts file — no forward declarations needed.

Practical fix-ups. After pasting the output, three edits typically follow: (1) widen any field whose JSON value was null to T | null using a non-null sample as the basis for T; (2) mark optional fields with a trailing ? if your API spec says so; (3) consider replacing ISO-8601 string fields with a branded type like type ISODateString = string & { _brand: 'ISODateString' } if you want the compiler to prevent accidental concatenation of arbitrary strings into date-typed fields.

Common Use Cases

Onboarding to an Existing JSON API

You're integrating a third-party API that ships an OpenAPI spec only as a PDF, or no spec at all. The fastest path to type safety is to call one endpoint, paste the response into this tool, and use the generated interfaces as the initial type contract. Iterate by pasting additional sample responses and reconciling the differences into unions or optional fields. After a handful of representative samples, you have a working type definition that compiles, autocompletes in your IDE, and surfaces field typos at build time — without waiting for the vendor to publish a proper machine-readable spec.

Generating Types from Sample Responses

For internal microservices that already expose a JSON endpoint but lack TypeScript bindings, sample-driven generation is the most pragmatic bootstrap path. Capture a representative response from a development environment, paste it here, save the output to types/api.ts, and reference the generated types from your fetch wrapper. This pattern is most useful when the producing service is written in a different language (Go, Python, Ruby) and there's no shared schema repository — the consumer just snapshots the wire format and goes.

OpenAPI Gap-Filling

OpenAPI specs sometimes describe response shapes loosely (type: object with no properties) or rely on oneOf/anyOf in ways that codegen tools handle poorly. When the spec is incomplete, generating types from a real sample response fills the gap. The workflow: use openapi-typescript for the bulk of the surface, then patch in hand-generated types from this tool for the few endpoints whose spec entries are too vague to codegen cleanly.

Mock Data → Real Types

Frontend teams often build against mocked JSON before the backend ships. The mock files in test/fixtures/ are themselves the de facto spec during that phase. Running each fixture through this tool produces TypeScript types that match the mocks exactly, which means the frontend's compile-time guarantees stay in lockstep with whatever data the mock service is returning — and when the real backend ships, swapping the fetch URL is enough; the types are already correct.

Drafting a Schema from an Existing Dataset

When you inherit a dataset — a JSON-Lines log file, a NoSQL collection export, a webhook archive — and want to bring it under static-type control, sample-driven type generation is again the natural starting point. Pick a representative record, paste it here, and the generated interface becomes the seed for a hand-maintained schema. Pair it with the JSON Schema Validator if you want both compile-time TypeScript checks and runtime JSON Schema validation against the same source of truth.

Edge Cases and Limitations

Sample-driven type inference necessarily has blind spots. The tool can only see what the JSON literally contains; everything else — optionality, runtime polymorphism, recursion, semantic typing — has to be reconstructed by hand after generation. The list below covers the cases that catch users by surprise most often.

Empty arrays cannot be inferred. "tags": [] contains zero elements, so the tool has nothing to infer the element type from. The output falls back to unknown[] in strict modes and any[] otherwise. The fix is to include at least one representative element in your sample — even a single value unlocks accurate element-type inference. If you cannot modify the sample, hand-edit the generated type to declare the intended element: tags: string[].

Heterogeneous arrays are ambiguous. An array like ["a", 1] could be inferred as (string | number)[] (a union element type) or as a tuple [string, number] (positional fixed-arity). Both interpretations are technically valid TypeScript; they describe very different runtime behaviors. The tool conservatively reads the first element only and uses that as the element type, which can produce wrong results for genuinely mixed arrays. For tuple semantics, hand-edit to [string, number]; for union semantics, hand-edit to (string | number)[]. There is no purely sample-driven way to disambiguate intent.

Null vs. undefined is opinionated in JSON. JSON has null but not undefined — fields that "don't exist" are simply absent from the document. The tool reads a missing key as "not in the type" (the property doesn't appear in the generated interface) and a present-but-null key as fieldName: null. TypeScript's exactOptionalPropertyTypes compiler option matters here: with it enabled, profile: Profile | null and profile?: Profile are distinct shapes (one requires the key with a possible null, the other allows the key to be absent entirely). The tool cannot infer the difference from a single sample — pick the variant your runtime actually produces.

ISO 8601 strings stay as string. A value like "2025-01-15T10:30:00Z" looks like a date, but JSON's type system is just string. The tool emits string, which is technically correct: JSON.parse() hands you a string, not a Date. Declaring the field as Date in the type would be a lie — the value at runtime is still a string until you call new Date(value) on it. Branded-string patterns (type ISODateString = string & { _brand: 'ISO' }) are the most defensible workaround when you want compiler-level discrimination between arbitrary strings and timestamps.

Recursive types are not detected. A self-referential structure like { "value": 1, "next": { "value": 2, "next": null } } describes a linked list. TypeScript supports recursive types directly (interface Node { value: number; next: Node | null; }), but the tool generates one fresh interface per nesting level rather than recognizing the recurrence. After generation, manually consolidate identical shapes by replacing nested references with the parent type name.

Property name collisions. When two nested objects at different paths share a parent key (e.g., the JSON contains both address.line1 and shipping.line1), the tool generates two separate interfaces (Address and Shipping) which is correct. But two siblings with the same key but different shapes — say, two nested config objects with different fields — produce a type-name collision on Config. The tool sidesteps duplicates by checking the name list; the first one wins. Manually rename the second one in the output to disambiguate.

Numeric-looking string keys. JSON object keys are always strings, even when they look like numbers: {"42": "answer"} generates "42": string;. TypeScript accepts this as a quoted property name, but if you intended a numeric index signature ({ [key: number]: string }), hand-edit accordingly. Pascal-casing also produces unusual identifiers from numeric-prefixed keys; review the output for keys that started with digits.

Pascal vs. camel case. The tool Pascal-cases nested type names (the interface name itself) but preserves the original casing of property keys. So user_profile as a JSON key becomes user_profile: UserProfile — the key is unchanged, the type name is normalized. If your codebase converts to camelCase at the fetch boundary, also rename the keys in the generated interface.

Behind the Scenes: TypeScript's Type System vs. JSON's

JSON Schema vs. TypeScript Type System

JSON Schema and TypeScript types overlap heavily but are not equivalent. JSON Schema is a runtime contract language — it ships with the data, validates inputs at the network boundary, and supports keywords like minLength, pattern, format, and oneOf with full discriminator semantics. TypeScript types, by contrast, are erased at compile time: nothing about a declared email: string survives into the running JavaScript, so a runtime payload with email: 42 will pass through every TypeScript check and explode at the first downstream .toLowerCase() call. The two systems serve complementary purposes — TypeScript for IDE autocompletion and refactor safety, JSON Schema (or Zod, io-ts, Valibot) for boundary validation. Generated types from this tool are one half of that pair; you still want a runtime validator at every untrusted ingress point.

Structural vs. Nominal Typing

TypeScript uses structural typing: two types are interchangeable if they have the same shape, regardless of name. An object literal with the right keys and value types is assignable to any interface whose declared shape matches, even with no explicit declaration of intent. This is convenient for type inference (which is why this tool can work — sample shape is the type definition) but it also means TypeScript cannot distinguish between a UserId and an arbitrary number unless you opt into nominal-like patterns via branded types: type UserId = number & { _brand: 'UserId' }. Branded types are not inferable from JSON samples — you add them after generation when you want compiler-enforced discrimination between same-shaped values that mean different things.

Why TypeScript Can't Enforce JSON Shape at Runtime

TypeScript types disappear at tsc compile time — the emitted JavaScript contains no type information whatsoever. This is intentional: TypeScript is a structural superset of JavaScript with zero runtime cost. The trade-off is that fetch().then(r => r.json() as MyType) is a lie the compiler trusts unconditionally. If the server returns a payload that doesn't match MyType, the cast silently succeeds, and the mismatch surfaces as a baffling undefined access many call frames downstream. To bridge the gap, pair generated types with a validator: Zod, io-ts, Valibot, Yup, and Joi all parse JSON into typed values and throw on mismatch. The pattern is const data = MyTypeSchema.parse(await response.json()), which gives you both runtime safety and the inferred static type via z.infer<typeof MyTypeSchema>.

How as const Narrows Literals

By default, TypeScript widens literal types in object expressions: { status: "ok" } has type { status: string }, not { status: "ok" }. The as const assertion freezes literals at their narrowest type: { status: "ok" } as const has type { readonly status: "ok" }. This matters for discriminated unions — a function that returns a tagged-union response cannot drive type narrowing on the consumer side unless the discriminator is preserved as a literal. Generated types from this tool always widen to string, never to the specific literal value. If you want literal discrimination, hand-edit the generated type to use the literal value directly: status: "success" | "error".

infer and Conditional Types

TypeScript's infer keyword, used inside conditional types, extracts type fragments from generic constraints: type ElementOf<T> = T extends Array<infer U> ? U : never pulls the element type out of any array. Combined with typeof, infer enables sophisticated type derivation — for example, deriving the return type of an async function as the resolved value: type Awaited<T> = T extends Promise<infer U> ? U : T. None of this is needed for the simple JSON-to-types transformation this tool performs; it's the layer above, where generated types get composed into a real codebase. Once you have the base interfaces, infer-based conditional types let you derive everything else mechanically rather than maintaining parallel type chains by hand.

Comparison: This Tool vs. Quicktype vs. json-to-ts vs. ts-json-schema-generator vs. Zod inference

JSON-to-TypeScript codegen is a crowded space. Each tool sits at a different point on the spectrum from "lightweight clipboard converter" to "full schema-driven build pipeline." The summary below clarifies when each is the right tool.

JSON-to-TypeScript Conversion Tools: When Each Fits
Tool Input Type Runtime? Best For Limitations
This tool JSON sample (paste) Browser-only One-off conversion, exploration, quick prototypes Single-sample inference; no optional detection; no recursion detection
quicktype.io JSON sample or JSON Schema Browser or CLI Multi-language targets (TypeScript, C#, Go, Swift, Rust, etc.); union inference from multiple samples Heavier UI; generated output can be verbose for simple cases
json-to-ts (npm) JSON sample Node CLI / programmatic CI pipelines; programmatic conversion of many samples in scripts No web UI; TypeScript-only output
ts-json-schema-generator TypeScript type (reverse direction) Node CLI Generating JSON Schema from TypeScript for runtime validation, OpenAPI spec output Opposite direction; requires TypeScript as source of truth
Zod z.infer<typeof Schema> Zod schema (hand-authored) Runtime parse + compile-time inference Single source of truth for runtime validation and static types; production API boundaries Schema must be hand-written; no JSON-to-schema codegen built in
Tools listed by typical workflow position. This tool and quicktype handle the "I have a sample, want types" entry point; ts-json-schema-generator runs the reverse direction; Zod's z.infer is the runtime-safe end state most production codebases converge toward.

When to use this tool. Single sample, one-off conversion, no install, no signup, no upload — you have a JSON blob in your clipboard and want types in your editor 10 seconds from now. The output is straightforward TypeScript that compiles immediately.

When to use quicktype. You have multiple samples and want union/optional inference across them. You need multi-language output (the same JSON should also produce Go structs, C# classes, Rust structs). The web UI is comparable, but quicktype's CLI handles batch workflows that this tool doesn't address.

When to use json-to-ts (npm). You're scripting a build step that converts a directory of fixtures to TypeScript types in CI. Programmatic conversion in Node is the deciding factor — this is the same conversion this tool performs, but headless.

When to use ts-json-schema-generator. Your codebase is TypeScript-first and you need to emit JSON Schema for runtime validators, OpenAPI specs, or downstream consumers in other languages. This is the reverse direction — TypeScript is the source of truth, schema is the artifact.

When to use Zod's z.infer. Production API boundary. You want a single declaration that simultaneously validates JSON at runtime and produces the matching static type via z.infer<typeof Schema>. The Zod schema is hand-authored, not generated — but it's the most durable pattern for codebases that handle untrusted input. The typical flow is: use this tool or quicktype for the initial draft, manually translate the generated TypeScript into a Zod schema, then let Zod become the source of truth going forward.

Frequently Asked Questions

Generated TypeScript types are erased at compile time — they offer zero runtime protection against a server returning a payload that doesn't match the declared shape. To enforce the contract at runtime, pair the generated types with a validation library such as Zod, io-ts, or Valibot. The pattern is to define a schema with z.object({...}), run fetch().then(r => r.json()).then(Schema.parse), and let z.infer<typeof Schema> derive the static type from the schema. If the server drifts, parse() throws at the boundary instead of producing silent undefined access deep in the call stack.
Interfaces describe object shapes and support declaration merging — multiple interface blocks with the same name automatically combine into one. They can also extend other interfaces with the extends keyword. Type aliases assign a name to any type, including unions, intersections, tuples, mapped types, and conditional types — things interfaces cannot express directly. For plain object shapes that may be augmented by consumers (library typings, ambient declarations), use interface. For unions, discriminated tagged-union patterns, and computed types, use type. Compiled output is identical.
An empty array carries no element values, so structural inference has nothing to read. The tool defaults to unknown[] in strict modes and any[] otherwise. unknown[] is safer because it forces a narrowing assertion before element access, but it can be noisy when you know the intended shape. If your sample omits an element, the most reliable fix is to include at least one representative item — even a single example unlocks full element-type inference. Alternatively, hand-edit the generated type to declare the intended element type explicitly: tags: string[] rather than tags: unknown[].
This tool emits TypeScript type definitions only. To get Zod schemas, run the generated types through a downstream codegen pass — typescript-to-zod and ts-to-zod are the two most-used CLI packages — or use quicktype.io with a Zod target. The recommended workflow: keep the TypeScript type as the canonical source, derive the Zod schema once, and from that point forward use z.infer<typeof Schema> to project the schema back into a type. That preserves a single source of truth for both compile-time and runtime checks.
A single JSON sample cannot tell the tool which fields are optional — every key present in the sample is treated as required. If you have multiple sample payloads and some fields appear in only a subset, the recommended workflow is to merge the samples into a union sample, run the tool once, then hand-mark sometimes-absent fields with a trailing ? in the generated interface: profileImage?: string. For API specs with first-class optional metadata (OpenAPI, JSON Schema), use a converter that consumes the spec directly — ts-json-schema-generator preserves the optional flag, ours cannot infer it.
When a field's value type differs across samples (sometimes string, sometimes number), TypeScript expresses that with the | union syntax: id: string | number. This tool infers from one sample at a time, so the generated type reflects only that single shape. For multi-sample inference, run the tool once per representative payload and hand-merge the differing fields into unions. For discriminated unions (tagged variants where a literal field distinguishes shapes — type: 'success' vs type: 'error'), the pattern is to define one interface per variant and union them: type Response = SuccessResponse | ErrorResponse.
TypeScript fully supports recursive type definitions — interface TreeNode { children: TreeNode[]; } compiles without issue. Inferring recursion from a JSON sample, however, requires the tool to detect when a nested object's shape is structurally identical to an ancestor and reuse the parent type name rather than emitting a new one. This tool emits a fresh named type for every nested object level, so a 3-deep linked list will produce three distinct interfaces instead of one self-referencing interface. After generation, manually consolidate identical shapes and replace child references with the parent type name.
Three durable strategies. (1) If the API publishes an OpenAPI or JSON Schema spec, run a spec-driven codegen tool (openapi-typescript, ts-json-schema-generator) in CI so every spec change regenerates the types automatically. (2) If only sample payloads are available, commit a representative response fixture to your repo and re-run this tool on every fixture update — a pre-commit hook can diff the generated output against the committed type file. (3) Pair the generated type with a runtime validator (Zod, io-ts) at the fetch boundary so drift becomes an exception at runtime rather than a silent type mismatch downstream.