What this tool does
This tool takes a JSON document — typically an API response, a config file, or any structured data — and produces a matching set of TypeScript interfaces (or type aliases) that describe its shape. Paste JSON on the left, get types on the right, copy them into your codebase. The conversion runs server-side and returns in a few milliseconds; nothing is logged, nothing is persisted, and your input is discarded the moment the response is sent.
The most common workflow is integrating a third-party API. You hit the endpoint with curl or Postman, copy the response body, paste it here, get the TypeScript declarations, and now your editor autocompletes every field — no more response.data.user.firstName typos that show up as runtime errors at 11 PM. It's the kind of small thing that saves hours over a year of building backends.
When you actually need this
- Integrating a third-party REST or GraphQL API where the provider doesn't ship official TypeScript types (or the ones they ship are broken). Stripe, Slack, GitHub, and AWS all have official typings; smaller services often don't.
- Codegen from a real production response when the OpenAPI spec lies — which is more often than vendors admit. The actual JSON wire format is what your code will receive at runtime; type from that, not from documentation.
- Modeling a config file or YAML/JSON manifest that your application reads at startup. Validate the shape at the boundary, then trust it everywhere downstream.
- Migrating an untyped JavaScript codebase to TypeScript. Generating types from existing sample data is faster than writing them by hand for every API call.
- Onboarding to a new internal API that someone else built and forgot to document.
- Building a quick prototype where you need types but don't want to set up a code-generation pipeline yet.
How the inference works
The engine walks your JSON depth-first. For each primitive value it emits the obvious type — string, number, boolean, null. For each object it generates a named interface, deriving the name from the parent property (a property called user becomes interface User). For an array of objects, it merges all the samples into a single interface — fields that appear in every sample become required, fields that appear in some become optional with the ? marker.
Array property names are auto-singularized: users: User[], categories: Category[], indices: Index[]. Name collisions get a numeric suffix — two interfaces both wanting to be called Address become Address and Address2. Property keys that aren't valid TypeScript identifiers (anything with hyphens, dots, or leading digits) get wrapped in quotes automatically.
Heterogeneous arrays — [1, "two", true] — produce union element types: (boolean | number | string)[]. Empty arrays become any[] because there's literally no information to work with. If your real data has empty arrays in some samples but populated arrays in others, the merge step preserves the populated type.
Practical examples
Example 1 — Paginated API response
{
"page": 1, "total": 132,
"data": [
{ "id": "u_1", "name": "Alice", "verified": true },
{ "id": "u_2", "name": "Bob" }
]
}
Produces a Root interface with page, total, and data: Datum[], plus a Datum interface where verified is correctly marked optional because Bob doesn't have it. Notice you can rename Datum to something meaningful — the engine just inferred from the property name data. Rename the root from Root to UsersResponse via the options panel and re-convert.
Example 2 — Stripe-like payment intent
{
"id": "pi_3MtwBwLkdIwHu7ix28a3tqPa",
"amount": 2099,
"currency": "usd",
"customer": { "id": "cus_NfFG7M3PnQJ4Ka", "email": "a@example.com" },
"metadata": {},
"payment_method_types": ["card", "us_bank_account"]
}
The output gives you four declarations: Root with the top-level fields, Customer for the nested object, and the string-array for payment methods. Note metadata: {} — an empty object becomes an empty interface. In real code you almost always want to widen this to Record<string, string> manually — the engine can't infer that intent from a single empty sample.
Example 3 — heterogeneous webhook payload
Webhooks that batch multiple event types in one array ([{"type":"order.created", …}, {"type":"refund.created", …}]) produce a merged interface where the union of all possible fields is present, with most marked optional. This is technically correct but ergonomically painful — for real codegen you'd typically want a discriminated union. The pragmatic path: copy the merged interface, then refactor it into a discriminated union by hand using type WebhookEvent = OrderCreated | RefundCreated. The tool gets you 80% of the way there.
Best practices
- Generate from a real response, not the documentation. Hit the endpoint, dump the actual JSON, paste it here. Documentation lies, especially around optional fields and null vs missing.
- Generate from multiple samples when possible. A single sample can't tell the engine that a field is optional. If your endpoint returns lists, paste a list of at least 3–5 varied items; the optional-field detection will work properly.
- Use
interfacefor object shapes,typefor unions and intersections. TypeScript treats them slightly differently — interfaces support declaration merging, types support union/intersection composition. The tool defaults to interface; switch to type via the options panel when you specifically need it. - Treat the output as a starting point, not gospel. The engine doesn't know that
emailshould be a brandedEmailtype, or thatidvalues are opaque strings that shouldn't be compared with other IDs, or that astatusfield is actually a string-literal union. Add those refinements by hand once. - Watch for
any[]from empty samples. If your output hasany[]anywhere, your source JSON had an empty array. Either find a sample with content, or replaceany[]manually.
Common pitfalls
- "Number" can hide an integer-vs-float distinction. TypeScript has no separate
inttype.id: numbercovers both1and1.5. If your API contract guarantees integers, document it with a JSDoc comment or use a branded type. - Dates appear as
string. JSON has no date type. An ISO 8601 timestamp like"2026-05-14T10:30:00Z"is just a string to the JSON parser, and that's how it gets typed. Convert toDateat your domain boundary (the function that callsfetch), not in your model. - Nullable fields require a sample with null. If your API returns
email: nullin some responses but the sample you provided only hademail: "x@y.com", the inferred type isstring, notstring | null. Always sample a row where the optional field is null. - Recursive structures aren't detected. A tree of
{ value, children: [...] }won't get a self-referencing type — each level gets its own interface (Children, GrandChildren, etc.). For recursive data, write the type by hand using atype Node = { value: string; children: Node[] }pattern. - JSON numbers can lose precision. JavaScript's
JSON.parserepresents all numbers as 64-bit floats. A 19-digit integer ID (Twitter snowflake IDs, for example) gets silently truncated. Type those asstringin your API contract and parse them as strings everywhere.
Developer workflow integration
For one-off conversions, this tool is faster than installing a generator and configuring it. For repeatable generation in a project, the right answer changes:
- OpenAPI-described APIs: use
openapi-typescriptagainst the spec — it preserves nullability, enums, and oneOf in ways manual inference can't match. - GraphQL APIs:
graphql-codegengenerates types from the schema and your operations, with full type-safe variables and result types. - Internal services without a spec: use this tool to bootstrap, then commit the generated types to your repo. Re-run when the API changes; diff the output.
- Hand-authored DTOs: for objects you control, write the type first and parse JSON through Zod or io-ts. The runtime validation is the point — type inference can't catch a server that lied about its response shape.
Troubleshooting
- "Invalid JSON at line X" error. The parser caught a syntax error. Common causes: unquoted keys (JSON requires
"key":, notkey:), trailing commas, single quotes instead of double, comments (JSON doesn't allow them). Run your input through the JSON formatter first if you're unsure. - Output has dozens of weird interface names. If you generated from a deeply-nested response, every level gets its own interface. Rename them in your editor after copying — the engine names from property paths, which isn't always what humans would pick.
- Two unrelated objects merged into one interface. If you pasted an array where the elements have different shapes (truly different — not just optional fields), the smart-merge produces a single interface with everything optional. Turn off "smart optional fields" in the options panel and re-convert to see each shape separately.
- "any[]" everywhere. All your arrays in the sample were empty. Find or construct a sample where the arrays have at least one element.
Common use cases
- Type a third-party REST or GraphQL API response in seconds
- Bootstrap TypeScript types when migrating an untyped JavaScript codebase
- Generate interface declarations from a real production response when the OpenAPI spec is wrong
- Model a configuration file (JSON or YAML-converted) so it autocompletes everywhere
- Onboard to a new internal API that nobody documented
Frequently asked questions
Does it produce <code>interface</code> or <code>type</code>?
Both — switch via the options panel. Interfaces are the default because they support declaration merging (useful when extending third-party types). Type aliases are better when you need union/intersection composition.
How does it decide which fields are optional?
When you provide an array of objects, the tool merges them into a single interface and marks any field that doesn't appear in every sample as optional with <code>?</code>. With a single object as input, every field is required by default. Provide more samples for better optional-field detection.
My API returns <code>null</code> sometimes — why isn't the type <code>string | null</code>?
The inferrer only sees what's in your sample. If your sample contains <code>"email": "x@y.com"</code> but not <code>"email": null</code>, the inferred type is <code>string</code>. Paste a sample that includes a null value to get <code>string | null</code>.
How are array element names chosen?
They're singularized from the property name. A property called <code>users</code> produces an interface named <code>User</code>. Special cases handled: <code>categories</code> → <code>Category</code>, <code>indices</code> → <code>Index</code>, <code>analyses</code> → <code>Analysis</code>. The "status" suffix is left alone because it's already singular.
Why does my type have <code>any[]</code>?
Your input had an empty array somewhere. The engine has no information about what an empty array might contain. Either find or construct a sample where the array has at least one element, or replace <code>any[]</code> manually with the type you know it should be.
Does it support dates, UUIDs, emails as branded types?
Not yet — those all appear as <code>string</code> because that's what they are in JSON. Adding them as branded types (<code>type ISODate = string & { __brand: "ISODate" }</code>) is something you typically do once by hand to enforce stronger boundaries in your codebase.
What's the maximum input size?
1 MB. For anything larger, run a generator locally — try <code>quicktype</code> or <code>json-to-ts</code> from npm.
Is my JSON logged?
No. The input is sent to our server to run the inference (it requires Node.js because some operations don't have reliable browser equivalents), held in memory for the duration of the request, and discarded immediately after the response is sent. Nothing is logged or persisted.