How to generate a json schema to drive typescript types
- Step 1Grab a complete response sample — Use a real
200 OKbody with every optional field populated. The generator only describes keys present in the sample, so a sparse response yields a sparse interface. For arrays, make sure the FIRST element carries all fields. - Step 2Load the JSON — Drop the
.jsonfile (free tier up to 2 MB) or paste it. The tool runs strictJSON.parseon one value — no NDJSON, trailing commas, or comments. - Step 3Name the root interface via Schema title — Set Schema title to the model name (default
MySchema), e.g.UserResponse. json2ts uses the roottitleas the top-level interface name. Nested objects are named by json2ts from their property keys, not from a title. - Step 4Keep required on for non-optional fields — Leave Mark all fields required on so every top-level key lands in
required; json2ts then emits each field without?. Turn it off, or prune the array later, for fields that should be optional in the interface. - Step 5Generate and download the schema — Click Generate Schema, then Download Schema (saves
<name>.schema.json, 2-space indent). Copy works too. The on-screen preview truncates past 5,000 characters, but the file is whole. - Step 6Run json2ts to emit the interfaces — Install with
npm i -D json-schema-to-typescript, thennpx json2ts UserResponse.schema.json > types.ts. Convert any"type":"null"fields to nullable unions first so the generated type isstring | null, notnull.
Schema feature → resulting TypeScript (via json2ts)
How the generated Draft-07 schema maps to TypeScript through json-schema-to-typescript. Defaults shown.
| Schema fragment | TypeScript output | Notes |
|---|---|---|
{"type":"string"} | string | No literal/enum unless you add enum by hand |
{"type":"integer"} / {"type":"number"} | number | TypeScript has one numeric type; both map to number |
{"type":"boolean"} | boolean | — |
{"type":"null"} | null | From a null sample value — usually wrong; change to ["string","null"] for string | null |
key in required | non-optional field | Drop the key from required to get an optional field?: |
key NOT in required | field?: (optional) | Turn off the required checkbox, or prune, to mark fields optional |
{"type":"array","items":{...}} | T[] | Item type follows the array merge rules |
{"type":"array","items":{}} | unknown[] | Empty array sample → items:{} → loose element type |
Generator behaviors that change your interfaces
Each of these affects the TypeScript json2ts produces. Verify against your sample before generating types.
| Behavior | What you get | Fix for accurate types |
|---|---|---|
| Array item merge by top-level type | Only the first element's keys appear in items.properties | Generate from an object whose first array element has all fields |
| null → type:null | json2ts emits null not a nullable union | Edit property to {"type":["string","null"]} |
| No enum inference | Status strings become string, not a literal union | Add "enum":["active","paused"] for a 'active' | 'paused' type |
Nested objects have no required | Nested object fields are emitted optional by json2ts | Add a required array to each nested object to make fields non-optional |
| title is root-only | Nested interfaces get json2ts-generated names | Rename in the output types.ts, or add title per nested object manually |
Cookbook
Realistic API responses, the schema they generate, and the json2ts result — including the two edits that turn loose types into the strict interfaces you want.
Response sample to UserResponse interface
ExampleA typical user record generates a clean schema; json2ts turns it into a non-optional interface because required was on.
Sample: { "id": 42, "name": "Sue", "verified": true }
Schema title: UserResponse · required: ON
Generated schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "UserResponse",
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"verified": { "type": "boolean" }
},
"required": ["id", "name", "verified"],
"additionalProperties": false
}
npx json2ts UserResponse.schema.json:
export interface UserResponse {
id: number;
name: string;
verified: boolean;
}Turn a null field into string | null
ExampleA null sample value generates "type":"null", which json2ts emits as null. Convert to a union before running json2ts.
Sample: { "name": "Sue", "avatarUrl": null }
Generated (json2ts would emit avatarUrl: null):
"avatarUrl": { "type": "null" }
Edit to:
"avatarUrl": { "type": ["string", "null"] }
json2ts now emits:
avatarUrl: string | null;Add an enum for a literal union type
ExampleA status string infers "type":"string". Add an enum by hand to get a TypeScript string-literal union.
Generated:
"status": { "type": "string" }
Edit to:
"status": { "type": "string", "enum": ["active", "paused", "closed"] }
json2ts now emits:
status: 'active' | 'paused' | 'closed';Array of items keeping all keys
ExampleBecause item schemas merge on top-level type, only the first element's keys survive. Put a complete object first.
Risky sample (first item missing 'email'):
"users": [ { "id": 1 }, { "id": 2, "email": "x@y.com" } ]
=> items.properties has only { id }, so User has no email.
Fix — first element complete:
"users": [ { "id": 1, "email": "x@y.com" }, { "id": 2, "email": "a@b.com" } ]
=> items.properties has { id, email }, User gains email.Schema-first vs direct conversion
ExampleWhen you do not need a versioned schema, skip straight to types with the dedicated tool — it reads object key unions across array items, which the schema generator does not.
Schema route (versioned contract): generate schema here -> commit -> npx json2ts -> types.ts Direct route (fastest, deep array key union): paste sample into /tool/json-to-typescript -> copy interface Use schema-first when backend + frontend share the contract; use direct when you just need the interface now.
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.
A null sample value yields type:null and json2ts emits `null`
By designavatarUrl: null infers { "type": "null" }; json2ts turns that into the literal type null, not string | null. Edit the property to { "type": ["string", "null"] } before running json2ts. Scan every null in your sample — there is no nullable inference.
Array of objects loses keys not on the first element
Partial schemaItem schemas merge by top-level type; for an array of objects the merge returns the FIRST element's schema. Keys present only on later elements are dropped from items.properties, so the generated interface is missing them. Order a complete object first, or use the JSON to TypeScript tool, which unions object keys across array elements.
Status strings become string, not literal unions
Not inferredNo enum is inferred from sample values, so a status: "active" field becomes "type":"string" and json2ts emits string. Add "enum": ["active","paused"] to the property to get a 'active' | 'paused' literal union in TypeScript.
Nested object fields come out optional
Nested looseOnly the ROOT object receives a required array. Nested objects have none, so json2ts marks their fields optional (field?:). To make nested fields non-optional, add a required array listing the keys to each nested object schema manually.
Integer vs number both map to TypeScript number
Expected30 infers integer and 19.99 infers number; TypeScript has a single number type, so both become number. The integer/number distinction is preserved in the schema (useful for runtime validation) even though it collapses in the types.
Empty array sample produces unknown[]
ExpectedAn empty array infers items: {} (matches anything), which json2ts renders as unknown[] (or any[] depending on config). Provide at least one populated element so the item type is concrete. The first element drives the inferred item schema.
NDJSON or JSONL response logs fail to parse
Parse errorThese extensions are accepted, but the tool parses the file as one JSON value. A line-delimited file throws Unexpected non-whitespace character after JSON. Wrap the lines in [ ... ], or take one record and generate from that single object.
Schema title only names the root interface
Root onlyThe Schema title is written to the root title only, so json2ts names the top-level interface after it. Nested interfaces are named by json2ts from their property keys. Rename them in the generated types.ts, or add a title to each nested object schema by hand.
Sample over the free 2 MB limit
Schema limitFree JSON processing caps at 2,000,000 bytes (2 MB). A full response dump is more than you need — one representative record produces the same interface. Pro lifts the cap to 100 MB if you must process a large file.
Frequently asked questions
How is this different from the JSON to TypeScript tool?
The JSON to TypeScript tool reads your sample and emits interfaces directly, and it unions object keys across array elements. This tool emits an intermediate JSON Schema you can version, share, and reuse for validation — you then run json2ts to get the types. Schema-first when the schema is the shared contract; direct when you just need types fast.
What command turns the schema into TypeScript?
Install json-schema-to-typescript (npm i -D json-schema-to-typescript) then run npx json2ts MySchema.schema.json > types.ts. The Draft-07 output of this generator is exactly what json2ts expects.
Why is my nullable field typed as null instead of string | null?
A null sample value infers "type":"null", which json2ts renders as the literal null. Edit the property to { "type": ["string", "null"] } before running json2ts to get string | null.
Why did a field from my array disappear in the interface?
Array item schemas merge by top-level type, so for an array of objects only the first element's keys are kept. Reorder so a complete object is first, or convert directly with the JSON to TypeScript tool, which merges keys across all elements.
How do I get a string-literal union like 'active' | 'paused'?
Add an enum to the property by hand — { "type": "string", "enum": ["active","paused"] }. The generator never infers enums from sample values, so json2ts emits a plain string until you add the enum.
How do I make a field optional in the interface?
Remove its key from the schema's required array (or generate with the required checkbox off). json2ts emits keys not listed in required as optional field?:.
Are integer and number different in the output types?
No. Both integer and number map to the single TypeScript number type. The distinction is preserved in the schema for runtime validation but collapses in the generated interfaces.
Can I change the output indentation?
Not from the UI — the schema is always 2-space indented. If you need a different format, reformat the downloaded file with the JSON prettifier or JSON minifier.
Should I use Zod instead of generating types via JSON Schema?
If you want a runtime validator that also infers static types, the JSON to Zod tool emits a Zod schema from your sample, and z.infer<typeof schema> gives you the type. Use JSON Schema + json2ts when the schema must be language-neutral and shared with non-TypeScript services.
Is my response sample uploaded anywhere?
No. Generation is 100% client-side. Response bodies, customer records, and field names never leave your browser.
Why did my .jsonl file fail to load?
The tool parses the file as a single JSON value, and JSONL has one object per line. That throws a parse error. Wrap the lines in an array, or generate from one line.
What is the free-tier size limit?
2 MB (2,000,000 bytes) per file, one file at a time. Pro raises it to 100 MB and 10 files. One sample record is normally a few KB.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.