@dreampulse/openapi-to-typescript
v1.3.0
Published
CLI tool that generates TypeScript type definitions from OpenAPI 3.x and Swagger 2.0 specs
Maintainers
Readme
openapi-to-typescript
Generates TypeScript type definitions and JSON Schemas from OpenAPI 3.x specs — one file per schema, one file per endpoint. The output is optimized for LLM consumption: small, self-contained files with all metadata preserved as JSDoc comments.
Why?
When working with LLMs on codebases that consume APIs, you want the AI to understand your API types without needing to parse a massive OpenAPI spec. This tool generates compact, individual .ts files that an LLM can read and reason about independently. Each file contains everything it needs: the type definition, JSDoc comments with descriptions, constraints, formats, and deprecation markers.
Design decisions:
typeinstead ofinterface— Union types, intersections, and mapped types compose better withtypealiases. This matters for discriminated unions andallOf/oneOfschemas.- One file per schema/endpoint — LLMs work best with small, focused context. A single file per type means you only feed the AI what it needs.
- All metadata as JSDoc — Descriptions, constraints (
@minimum,@maxLength,@pattern), formats (@format date-time), and deprecation are preserved as JSDoc comments so they're visible in IDE tooltips and to LLMs reading the code. - Unsupported features throw errors — Instead of silently generating wrong types, unsupported constructs (like
if/then/else) throw descriptive errors so you know what needs attention. - JSON Schema alongside TypeScript — Each schema also generates a
.schema.jsonfile (JSON Schema Draft 2020-12) with relative file$refs to its dependencies, useful for runtime validation. For LLM tool-use you can additionally emit a self-contained bundle with all dependencies inlined as$defs— see Bundle Mode.
Installation
pnpm add openapi-to-typescriptCLI Usage
npx openapi-to-ts --input <spec> --output <dir>| Option | Description |
|---|---|
| -i, --input <path> | Path to the OpenAPI spec (YAML or JSON) |
| -o, --output <dir> | Output directory for generated files |
| --single-file | Write all schemas into a single file (topologically sorted) |
| --bundle <name...> | Additionally emit <name>.bundled.schema.json — a self-contained JSON Schema with all transitive deps inlined as $defs. Repeatable. See Bundle Mode. |
Example:
npx openapi-to-ts -i ./api/openapi.yaml -o ./generatedThis reads the spec, resolves all internal $ref references, and writes the generated files:
generated/
schemas/
Kundenangaben.ts # export type Kundenangaben = { ... }
Kundenangaben.schema.json # JSON Schema with relative file refs
Anschrift.ts
Anschrift.schema.json
...
endpoints/
getKundenangaben.ts # path params + header params + response types
importKundenangaben.ts # header params + request body + response types
...The output directory is cleaned before each run.
Bundle Mode (LLM tool-use)
The per-schema .schema.json files reference each other via relative file paths
("$ref": "Bankverbindung.schema.json"). That is fine for browsing, but
unusable as a single JSON Schema document — which is exactly what the
Anthropic API expects for tool.input_schema
when you want Claude to extract structured data against your API types.
--bundle <name> emits an additional <name>.bundled.schema.json at the
output root, containing the named schema plus all transitive dependencies
inlined under a top-level $defs. All $refs are rewritten to internal
#/$defs/<Name> pointers, producing a self-contained, valid JSON Schema
Draft 2020-12 document.
npx openapi-to-ts -i ./api/openapi.yaml -o ./generated \
--bundle Kundenangaben \
--bundle KundenangabenImportResponseOutput shape:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "#/$defs/Kundenangaben",
"$defs": {
"Kundenangaben": { /* ... */ },
"Bankverbindung": { /* ... */ },
"Anschrift": { /* ... */ }
}
}Use it directly as a tool input schema:
import bundle from "./generated/Kundenangaben.bundled.schema.json" with { type: "json" };
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools: [
{
name: "extract_kundenangaben",
description: "Extract customer details into our API schema",
input_schema: bundle,
},
],
messages: [{ role: "user", content: "..." }],
});We use $defs rather than full dereferencing because reused and cyclic schemas
otherwise blow up the schema size — and tool-input schemas count against the
prompt token budget.
Bundle mode is additive: the per-schema .ts and .schema.json files are
still generated as usual.
Output
Schema files
Each schema from components/schemas becomes a .ts file with a single export type:
// schemas/Anschrift.ts
export type Anschrift = {
hausnummer?: string;
ort?: string;
/**
* Postleitzahlen, die nicht aus fünf Ziffern bestehen werden ignoriert.
* @pattern \d{5}
* @example "10557"
*/
plz?: string;
strasse?: string;
};Properties from the OpenAPI spec are preserved:
description→ JSDoc commentformat→@formattagdeprecated→@deprecatedtag- Constraints (
minimum,maxLength,pattern,minItems, ...) → corresponding@tag readOnly→readonlymodifierrequiredfields are non-optional, everything else gets?
Discriminated unions are handled automatically. A base schema with a discriminator mapping becomes a union type, and each subtype gets the discriminator property as a string literal:
// schemas/Finanzierungsbaustein.ts (base — union of subtypes)
export type Finanzierungsbaustein =
Annuitaetendarlehen | Bauspardarlehen | Forwarddarlehen | ...;
// schemas/Annuitaetendarlehen.ts (subtype — literal discriminator)
export type Annuitaetendarlehen = {
"@type": "ANNUITAETENDARLEHEN";
} & {
darlehensbetrag?: number;
annuitaetendetails?: Annuitaetendetails;
...
};Endpoint files
Each operation (identified by operationId) becomes a .ts file with types for parameters, request body, and responses:
// endpoints/getKundenangaben.ts
import type { Kundenangaben } from "../schemas/Kundenangaben";
import type { Problem } from "../schemas/Problem";
/**
* Kundenangaben für einen bestehenden Vorgang laden
* @http GET /kundenangaben/{vorgangsnummer}
*/
export type GetKundenangabenPathParams = {
vorgangsnummer: string;
};
export type GetKundenangabenHeaderParams = {
Authorization: string;
"X-TraceId"?: string;
};
export type GetKundenangabenResponse200 = Kundenangaben;
export type GetKundenangabenResponse401 = void;
export type GetKundenangabenResponse404 = Problem;Naming conventions:
- Schema files: PascalCase (matching the schema name from the spec)
- Endpoint files: camelCase (matching the
operationId) - Type names:
{PascalOperationId}PathParams,{PascalOperationId}QueryParams,{PascalOperationId}HeaderParams,{PascalOperationId}RequestBody,{PascalOperationId}Response{StatusCode} - Responses without a body become
void
JSON Schema files
Each schema also generates a .schema.json file following JSON Schema Draft 2020-12. References to other schemas use relative file paths (e.g. "$ref": "Bankverbindung.schema.json"), so each file stays small:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"plz": {
"type": "string",
"pattern": "\\d{5}",
"description": "Postleitzahlen, die nicht aus fünf Ziffern bestehen werden ignoriert.",
"examples": ["10557"]
}
}
}This is useful for runtime validation with libraries like Ajv. For a single self-contained schema document (e.g. as tool.input_schema for Claude), use Bundle Mode instead.
Library Usage
You can also use the tool programmatically:
import {
parseSpec,
generateFiles,
extractEndpoints,
schemaToType,
} from "openapi-to-typescript";Generate all files
import { parseSpec, generateFiles } from "openapi-to-typescript";
const spec = await parseSpec("./api/openapi.yaml");
await generateFiles(spec, "./generated");This is equivalent to the CLI — it parses the spec, generates all .ts and .schema.json files, and writes them to the output directory.
Extract endpoint metadata
import { parseSpec, extractEndpoints } from "openapi-to-typescript";
const spec = await parseSpec("./api/openapi.yaml");
const endpoints = extractEndpoints(spec);
for (const endpoint of endpoints) {
console.log(`${endpoint.method.toUpperCase()} ${endpoint.path}`);
console.log(` operationId: ${endpoint.operationId}`);
console.log(` parameters: ${endpoint.parameters.length}`);
console.log(` responses: ${endpoint.responses.map((r) => r.statusCode)}`);
}Each EndpointInfo contains:
operationId,method,path,summary,descriptionparameters— array of{ name, in, required, schema, description }requestBody— the request body schema (ornull)responses— array of{ statusCode, schema, description }
Convert a single schema to TypeScript
import { schemaToType } from "openapi-to-typescript";
schemaToType({ type: "string" });
// → "string"
schemaToType({ type: "object", properties: { name: { type: "string" } }, required: ["name"] });
// → '{\n name: string;\n}'
schemaToType({ enum: ["A", "B", "C"] });
// → '"A" | "B" | "C"'
schemaToType({ $ref: "#/components/schemas/Foo" });
// → "Foo"schemaToType handles: primitives, enums, const, objects (nested, with required/optional), arrays, tuples, allOf/oneOf/anyOf, nullable, readOnly, $ref, additionalProperties, and all JSDoc metadata. Unsupported constructs like if/then/else throw an error.
Supported OpenAPI Features
| Feature | TypeScript Output |
|---|---|
| type: "string" / "number" / "integer" / "boolean" | string / number / number / boolean |
| type: "null" (OAS 3.1) | null |
| nullable: true (OAS 3.0) | Type \| null |
| enum | "A" \| "B" \| "C" |
| const | "value" (literal type) |
| type: "object" with properties | { key: type; ... } |
| type: "array" with items | Type[] |
| prefixItems (tuples) | [Type1, Type2] |
| allOf | A & B (intersection) |
| oneOf / anyOf | A \| B (union) |
| discriminator with mapping | Discriminated union with string literal property |
| $ref | Named type import |
| additionalProperties | Record<string, Type> or intersection with object |
| readOnly | readonly modifier |
| description, deprecated, format, constraints | JSDoc comments |
