
JSON to TypeScript Converter
Paste JSON to generate TypeScript interfaces instantly. 100% client-side.
Last reviewed: April 2026New to this tool? Click here for instructions
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, 42 → number, true → boolean, null → null). 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
- Root object detection. The top-level value is an object, so the tool emits
interface RootObject { ... }. Its single propertyuserpoints at another object, which triggers recursion into a named child type. The parent property's type becomes the child interface name:user: User. - Primitive inference. Inside
User,id: 42maps tonumber(TypeScript has no separate integer type — all JSON numbers arenumber).email: "a@b.com"maps tostringregardless 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 isstringand the field becomesstring[]. - Date-like strings stay as string. The
createdvalue"2025-01-15T10:30:00Z"looks like an ISO 8601 timestamp, but JSON has no date type. The tool emitscreated: string, which is technically correct — at the JSON wire level, that value is a string. If you want aDateobject in your application, parse it after deserialization rather than declaring it asDatein the type (the type would lie, sinceJSON.parse()returns a string). - Null inference.
profile: nullemits the literal typenull. This is rarely what you want long-term —nullalone means "this field is always null," which is almost never accurate. Hand-widen the field toProfile | nullafter generation ifprofilecan sometimes hold an object. The tool also emits an emptyinterface Profile {}placeholder so the named type exists for reference; flesh it out from a non-null sample later. - Type ordering. Child types appear before their parents in the output (
Profilefirst, thenUser, thenRootObject). This guarantees the generated file compiles cleanly when pasted into a fresh.tsfile — 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.
| 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 |
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
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.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.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[].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.? 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.| 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.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.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.