How to generate typescript types for third-party sdk integration
- Step 1Capture a real payload — From a webhook handler, log the full body:
console.log(JSON.stringify(event, null, 2))and trigger a test event in the provider's dashboard (Stripe CLIstripe trigger, Shopify webhook tester). Or stringify an SDK response object the same way. Pick a complete event so optional fields are present. - Step 2Paste and name the event — Drop the JSON in and set 'Root type name' to the event/object, e.g.
StripePaymentIntentorShopifyOrderWebhook. Nested objects auto-name from their keys (customerbecomesCustomer). - Step 3Choose output style — Keep 'Use interface' on for SDK-style interfaces, or switch to
typeif you'll build anevent.typediscriminated union from several generated event shapes. Keep 'Export all types' on to gather them in one module. - Step 4Leave optional off for payloads — Webhook payloads are usually strict per event type — leave 'Optional properties' off. Toggle it on only for a loose draft; it marks every field
?and won't tell you which fields the provider actually omits. - Step 5Generate and place — Click 'Generate Types', then Copy or Download .ts into your integration's
typesfolder. - Step 6Refine for SDK semantics — Replace fixed-key
metadatablocks withRecord<string, string>, widenfield: nullplaceholders, and turn the literaltype: "payment_intent.succeeded"string into a union of the events you handle. Type the handler:const event: StripeWebhookEvent = await request.json();.
The four real generator options
These are the only controls in the tool's UI (app/tool/json-to-typescript). There is no indent toggle (output is always 2-space), no per-field optional detection, and no Date / branded-type inference — the generator maps JSON types to TS types one-to-one.
| UI control | What it does | Default | Notes |
|---|---|---|---|
| Root type name (text box) | Names the outermost type. Root by default; type User, ApiResponse, etc. | Root | Nested object types are named from their key (a address key becomes interface Address), not from this box |
Use interface (vs type) | Emits interface Name { ... } when on; type Name = { ... }; when off | On | Both forms are structurally identical; the toggle is purely stylistic |
| Export all types | Prefixes every emitted type with export | On | Turn off for types you'll re-export from a barrel file or keep module-private |
| Optional properties (?) | Adds ? to every field of every type at once | Off | This is a single global switch — it does NOT detect which keys are sometimes-missing. See the edge cases |
How JSON values map to TypeScript
The mapping is driven entirely by the JavaScript runtime type of each parsed value — there is no schema, no format detection, and no heuristics beyond these rules.
| JSON value | Generated TS type | Detail |
|---|---|---|
"text" | string | Every string is string — no string-literal union, no email/uuid/date-string narrowing |
42 or 9.5 | number | Integers and floats both become number. (The sibling json-to-zod does emit .int() for integers — TypeScript has no integer type, so this tool can't) |
true / false | boolean | No literal true/false narrowing |
null | null | A null value produces the literal type null — not string | null. There is no nullable inference |
{ ... } | named interface/type | Named from the key (or the root name). Emitted as a separate top-level type and referenced by name |
[ ] (empty) | unknown[] | An empty array can't be sampled, so the element type is unknown |
[1, 2, 3] | number[] | Element type inferred from the array contents |
[1, "a"] | (number | string)[] | Mixed-type arrays produce a union element type |
[{...}, {...}] | <Name>Item[] | Object arrays produce one item interface — sampled from the first element only |
Free vs Pro generation limits
JSON to TypeScript is a Pro tool gated by the shared codeGeneration quota (lib/preview-quotas.ts). Limits are daily-run counts plus a row cap that applies when the JSON root is an array.
| Tier | Runs/day | Array row cap | File size |
|---|---|---|---|
| Free | 1 | 100 rows | 2 MB |
| Pro | 5 | 1,000 rows | 100 MB |
| Pro Media | 50 | 10,000 rows | 100 MB |
| Developer / Enterprise | Unlimited | Unlimited | 100 MB+ |
Cookbook
Real provider payloads in, the verbatim TypeScript the generator emits out, and the SDK-specific edits an integration developer makes next.
Stripe-style nested object to named interfaces
ExampleA payment object with a nested customer and amount yields named Customer/root interfaces you reference across handlers.
Input:
{ "id": "pi_123", "amount": 1999, "currency": "usd", "customer": { "id": "cus_9", "email": "a@x.com" } }
Output (rootName: StripePaymentIntent):
export interface StripePaymentIntent {
id: string;
amount: number;
currency: string;
customer: Customer;
}
export interface Customer {
id: string;
email: string;
}metadata map needs Record<string, string>
ExampleStripe's metadata is an arbitrary string map, but the generator emits the literal keys from your sample. Replace with an index signature.
Input:
{ "id": "pi_1", "metadata": { "order_id": "42", "plan": "pro" } }
Output (verbatim):
export interface Root {
id: string;
metadata: Metadata;
}
export interface Metadata {
order_id: string;
plan: string;
}
Hand-edit: metadata: Record<string, string>; // arbitrary keysline_items array sampled from first item
ExampleAn order with multiple line items produces one item interface from the first element. If line items vary, reorder so the richest is first.
Input:
{ "line_items": [ { "sku": "A", "qty": 1 }, { "sku": "B", "qty": 2, "gift": true } ] }
Output:
export interface Root {
line_items: LineItemsItem[];
}
export interface LineItemsItem {
sku: string;
qty: number;
}
// 'gift' from item 2 is absent. Reorder the sample so the gift item is first.event.type discriminated union, by hand
ExampleGenerate each event you handle separately, then combine into a discriminated union keyed on the literal type.
Generate from payment_intent.succeeded → PaymentSucceededEvent
Generate from payment_intent.failed → PaymentFailedEvent
Combine by hand (narrow the type field to a literal):
type StripeWebhookEvent =
| (PaymentSucceededEvent & { type: "payment_intent.succeeded" })
| (PaymentFailedEvent & { type: "payment_intent.failed" });Type the webhook handler
ExampleApply the generated type at the request boundary, then switch on the (hand-narrowed) event type.
export async function POST(req: Request) {
const event: StripeWebhookEvent = await req.json();
switch (event.type) {
case "payment_intent.succeeded":
// event is narrowed to PaymentSucceededEvent
break;
}
}Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
metadata / tag maps become fixed-key interfaces
ExpectedProvider maps like Stripe metadata are emitted with the literal keys in your sample. Replace with Record<string, string> (or the right value type) since the real keys are arbitrary. The generator can't distinguish a map from a fixed-shape object.
Array payload sampled from first element
By designline_items, charges, and similar arrays infer their item interface from the first element only. Keys exclusive to later items are absent — reorder so the richest item is first, or paste a single merged superset object.
event.type not auto-unioned
By designThe literal event string becomes type: string and the generator never builds the discriminated union. Generate each handled event separately, then union them and narrow the type field to its literal by hand.
Polymorphic SDK field typed too narrowly
ExpectedA field that's an object in one event and null in another is typed from the single sample you pasted — e.g. null. Capture both variants and widen by hand, e.g. payment_method: PaymentMethod | null.
Nullable field typed as `null`
ExpectedAny null value yields the literal null type. Widen field: null to T | null manually, or capture a delivery where the field is populated to infer its type.
Expandable / sub-object reference
Depends on payloadStripe-style expandable fields are a string ID when collapsed and an object when expanded. The generated type matches whichever your sample contained. Decide which form your handler receives and capture that delivery, or hand-write string | CustomerObject.
Duplicate nested type names collide
First write winsIf two parts of the payload contain a Address object of different shapes, only the first interface is emitted and both reference it. Rename one JSON key before generating, or split the payload and generate separately.
Empty array field
unknown[]An empty refunds: [] becomes refunds: unknown[]. Replace with the real element type or capture an event where the array is populated.
Timestamps as Unix numbers vs ISO strings
ExpectedStripe sends created as a Unix-epoch number (number); other providers send ISO strings (string). The generated type matches the wire form — convert to Date in your handler as needed.
Invalid JSON pasted
Parse errorInput runs through JSON.parse after trimming; logs with surrounding text, trailing commas, or non-JSON wrappers throw. Isolate the pure JSON body (or fix with json-format-fixer) before regenerating.
Frequently asked questions
Should I use these generated types or the official @types package?
Use the maintained official types (e.g. stripe's bundled types) for the SDK client surface — they're complete and versioned. Reach for this tool when you need a precise interface for one webhook payload or a response sub-object, when the published types lag the API version you're pinned to, or when the official type is far broader than the slice your handler actually reads. The two coexist: official types for the client, generated narrow types for the events you process.
How do I handle Stripe's metadata, which is Record<string, string>?
The generator emits metadata as an interface with whatever literal keys were in your sample (e.g. { order_id: string; plan: string }). Replace that block with Record<string, string> (or { [k: string]: string }) by hand, because the real keys are arbitrary. This is true of any provider map — tag dictionaries, custom-field bags — the tool can't tell a map from a fixed-shape object from a single JSON value.
My JSON is an array of objects with varying keys — why are some fields missing from the type?
When the root (or any nested value) is an array of objects, the generator infers the item interface from the first element only. Keys that appear in later elements but not the first are absent from the generated type; keys in the first element but not later ones are present and non-optional. Reorder your sample so the most complete object is first, or merge a representative superset object by hand before pasting. This is the single most common surprise — the tool samples, it does not union across array elements.
How do I build the `event.type` discriminated union?
Generate each event you handle from its own sample payload (e.g. payment_intent.succeeded, payment_intent.failed), then combine them into a discriminated union, narrowing the type field from string to its literal: type Event = (Succeeded & { type: 'payment_intent.succeeded' }) | (Failed & { type: 'payment_intent.failed' });. The generator types one concrete payload per run and leaves the union to you.
A field came out as `: null` — that's not a useful type. Why?
JSON null maps to the literal TypeScript type null, because that's the only information present in the sample — the generator has no schema telling it the field is string | null. Wherever you see field: null, the real intent is almost always field: string | null (or another type) and you should widen it by hand. Capture a sample where the field has a non-null value if you want the underlying type inferred instead.
An expandable field is an ID in one payload and an object in another — how do I type it?
The generated type reflects exactly the form in your sample — a string if collapsed, a nested interface if expanded. Decide which your handler actually receives (driven by your expand parameters) and capture that delivery, or hand-write the union customer: string | Customer if both forms reach your code.
Two different nested objects share a key name — why did one shape get lost?
Nested object types are named from their key, and the generator registers each name only once (first write wins). If a meta object appears in two places with different shapes, both references point to the single Meta interface built from whichever was walked first; the second shape is silently discarded. Rename one of the JSON keys before generating, or generate the two branches separately and rename the resulting interfaces. The collision is structural, not a bug you can toggle off.
Should I keep 'Use interface' on, or switch to type aliases?
Structurally they're identical — interface Foo { ... } and type Foo = { ... } describe the same shape and the toggle changes nothing about the inferred fields. Interfaces support declaration merging and tend to produce cleaner compiler errors for object shapes, which is why they're the common default. Switch to type if you'll later combine the generated shape with unions or intersections (type X = Generated & { extra: string }), since that reads better starting from a type alias.
Will my webhook secret or customer PII be exposed by pasting a payload?
No. Parsing and type generation run entirely in your browser — the payload is never transmitted to JAD Apps. Only an anonymous run counter is recorded when you're signed in. That said, scrub secrets before sharing the generated types in a PR; the types themselves only contain field names and shapes, not values.
Can I stop the tool from exporting every type?
Yes — uncheck 'Export all types' and the export prefix is dropped from every emitted type. That's useful when you want a single barrel file to control your public surface, or when the generated types are module-private helpers. The toggle is all-or-nothing; there's no per-type export control, so for mixed visibility, generate with export off and add export to the specific types you want public.
How do I get the output into my project?
Use the Copy button to put the generated source on your clipboard, or Download .ts to save a file named after your input with a .types.ts extension (e.g. users.json becomes users.types.ts). The output is plain TypeScript source with one blank line between types — paste it straight into a .ts/.tsx file or a dedicated types.ts module.
What are the size and run limits?
JSON to TypeScript is a Pro tool gated by the shared codeGeneration quota. Free is 1 run per day; if the JSON root is an array, only the first 100 rows are sampled before generation. Pro is 5 runs/day with a 1,000-row sample cap; Pro Media is 50/day at 10,000 rows; Developer and Enterprise are unlimited. The file-size ceiling is 2 MB on free and 100 MB on paid tiers. The row cap only matters for array roots — a single deep object generates the same types regardless of tier because the shape, not the row count, drives the output.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.