Node.js API
The Node API may be useful if dealing with dynamically-created schemas, or you’re using within context of a larger application. Pass in either a JSON-friendly object or a YAML or JSON string to load a schema from memory, or a URL to load it from a local file or a remote URL.
Setup
npm i --save-dev openapi-typesafeAdd typescript (npm i --save-dev typescript) to type-check the generated file; the generator does not need it.
TypeScript is optional
The generator does not need the typescript package. Only callbacks or helpers that use TypeScript compiler nodes (the legacy forms, see IR callbacks) load it: typescript 6 or earlier, or @typescript/typescript6 in a TypeScript 7 project.
The declarations type-check with TypeScript 5.4 to 7 and skipLibCheck: false, except for four errors from the schema loader's own declarations; see Type-checking the declarations.
Recommended
For the best experience, use Node ESM by adding "type": "module" to package.json (docs)
Usage
The Node.js API accepts either a URL, string, or JSON object as input:
| Type | Description | Example |
|---|---|---|
URL | Read a local or remote file | await openapiTS(new URL('./schema.yaml', import.meta.url))await openapiTS(new URL('https://myapi.com/v1/openapi.yaml')) |
string | Read dynamic YAML or JSON | await openapiTS('openapi: "3.1" … ') |
JSON | Read dynamic JSON | await openapiTS({ openapi: '3.1', … }) |
A string that starts with file://, http:// or https:// is loaded like a URL; any other string, a file path included, is parsed as YAML or JSON. To load a file by its path, pass pathToFileURL(path) from node:url.
It also accepts Readable streams and Buffer types that are resolved and treated as strings (validation, bundling, and type generation can’t really happen without the whole document).
The Node API returns a Promise with the generated declarations as plain IR objects (IrDeclaration[]). Each one is a small object with a kind (interface, typeAlias, enum, constArray, rawDeclaration).
To print the declarations, use the astToString() helper. It prints IR with the package's own printer; given TypeScript nodes or TypeScript printer options, it falls back to TypeScript’s printer:
import fs from "node:fs";
import openapiTS, { astToString } from "openapi-typesafe";
const ast = await openapiTS(new URL("./my-schema.yaml", import.meta.url));
const contents = astToString(ast);
// (optional) write to file
fs.writeFileSync("./my-schema.ts", contents);Errors
When generation fails, openapiTS() rejects with an OpenAPITypesafeError. Each error has a stable code (for example SCHEMA_INVALID or SCHEMA_LOAD_FAILED) and structured details; cause holds the lower-level error, if any. Decide by code: messages are for people and may change. The codes are listed under CLI exit codes; openapiTS() never fails with the CLI's own codes (those that start with CLI_ or OUTPUT_). A minor release may add codes, so keep a fallback for a code you do not know. Of details, only these keys are stable; other keys may change in any release (Versioning):
| Code | Key | Type |
|---|---|---|
SCHEMA_INVALID | problems | string[]: one text per problem found |
SCHEMA_REF_FORBIDDEN | ref | string: the refused $ref as an absolute URL; user info shows as *** |
SCHEMA_REF_FORBIDDEN | reason | string: one of outside-local-root, host-not-allowed, local-ref-from-remote, too-many-remote-documents (see Untrusted documents); a minor release may add reasons |
details is typed as Readonly<Record<string, unknown>>, so check a value before you use it.
import openapiTS, { isOpenAPITypesafeError } from "openapi-typesafe";
try {
await openapiTS(new URL("./my-schema.yaml", import.meta.url));
} catch (error) {
if (isOpenAPITypesafeError(error) && error.code === "SCHEMA_INVALID") {
console.error(error.details.problems);
}
throw error;
}Deprecations
These keep working in 1.x and are removed in 2.0:
stringToAST(): return type text from an IR callback instead. It needs a TypeScript JavaScript API (typescript6 or earlier, or@typescript/typescript6).- The TypeScript-node form of
transform,postTransformandtransformProperty: use IR callbacks. resolveRef,scanDiscriminators,walk,c,debug,warnanderror, and the TypeScript node helpersaddJSDocComment,tsArrayLiteralExpression,tsDedupe,tsEnum,tsEnumMember,tsIsPrimitive,tsModifiers,tsPropertyIndex,tsReadonlyArray,QUESTION_TOKENandenumCache: rarely used internals, marked@deprecatedin the types (no runtime warning).
When one of the first two is used, Node.js prints a DeprecationWarning with one of these codes, once per process for each code. DEPRECATION_CODES exports them. Node's own switches work on them: node --no-deprecation hides them, --throw-deprecation makes them errors, --trace-deprecation shows where they come from, and process.on("warning", …) receives them. The silent option does not hide them.
| Code | Used |
|---|---|
OPENAPI_TYPESAFE_DEP_STRING_TO_AST | stringToAST() is called |
OPENAPI_TYPESAFE_DEP_TS_TRANSFORM | transform returns a TypeScript node |
OPENAPI_TYPESAFE_DEP_TS_POST_TRANSFORM | postTransform runs without irCallbacks: true |
OPENAPI_TYPESAFE_DEP_TS_TRANSFORM_PROPERTY | transformProperty runs without irCallbacks: true |
Redocly config
A Redocly config isn’t required to use openapi-typesafe. By default it extends the "minimal" built-in config. But if you want to modify the default settings, you’ll need to provide a fully-initialized Redocly config to the Node API. You can do this with the helpers in @redocly/openapi-core version 1, the version openapi-typesafe uses (npm i -D @redocly/openapi-core@1). A config made with version 2 has another shape and does not work: openapi-typesafe 1.x stays on Redocly 1 (ADR-005).
import { createConfig, loadConfig } from "@redocly/openapi-core";
import openapiTS from "openapi-typesafe";
// option 1: create an in-memory config
const inMemory = await createConfig({
extends: ["recommended"],
rules: { "operation-4xx-response": "off" },
});
// option 2: load it from a redocly.yaml file
const fromFile = await loadConfig({ configPath: "redocly.yaml" });
const ast = await openapiTS(mySchema, { redocly: inMemory });Options
The Node API takes the generator flags of the flags table in camelCase (--no-lint is lint: false; --output, --redocly, --check, --help and --version are CLI only), plus the following options:
| Name | Type | Default | Description |
|---|---|---|---|
transform | Function | Override the default Schema Object → TypeScript transformer in certain scenarios | |
postTransform | Function | Same as transform but runs after the TypeScript transformation | |
transformProperty | Function | Transform individual property signatures for Schema Object properties | |
irCallbacks | boolean | false | Callbacks receive and return IR nodes instead of TypeScript nodes (see IR callbacks) |
silent | boolean | false | Silence warning messages (fatal errors and deprecations will still show) |
lint | boolean | true | Lint the document with the Redocly config first. false skips it; bundling errors still fail |
cwd | string | URL | process.cwd() | The directory that relative $refs of a string, object, buffer or stream source resolve from. A file or URL source resolves from its own location |
localRefRoot | string | URL | Load local $ref files only from this directory and below it; a relative path resolves against cwd | |
remoteRefHosts | string[] | Load remote $ref documents only from these hosts (name or name:port); [] allows none | |
fix | OutputFixName[] | Opt-in fixes of the output, by name, or ["all"]; see fix. OUTPUT_FIXES lists them | |
inject | string | Inject arbitrary TypeScript types into the start of the file | |
redocly | Config | A Redocly config object; see Redocly config. Without it, Redocly's minimal config with operation-operationId-unique as an error (the CLI makes it a warning); the Node API does not look for redocly.yaml | |
version | number | Accepted for compatibility with openapi-typescript; no code reads it |
Options types
To type an options object, use GeneratorOptions: it takes either callback form, and it stays the name of the options in 2.0. OpenAPITSOptions (openapi-typescript's name, the TypeScript-node form) and OpenAPITSIrOptions (the IR form) are compatibility names; in 2.0 both become deprecated aliases of GeneratorOptions. In 2.0 irCallbacks: true is still accepted and does nothing, with a deprecation warning, so code that set it in 1.x keeps compiling; it is removed in 3.0.
IR callbacks
With irCallbacks: true, transform, postTransform and transformProperty work on the generator’s own type nodes (IR) instead of TypeScript compiler nodes, and you don’t need the typescript package. The TypeScript-node form (transform returning a TypeScript node, postTransform and transformProperty without irCallbacks) still works in 1.x, emits a Node.js DeprecationWarning (see Deprecations), and is removed in 2.0. The examples below use the IR form.
- A type result is an IR type (from the helpers, such as
tsUnion([STRING, NULL])) or a string of TypeScript type text, printed as written. transformmay also return{ schema, questionToken: true }to make the property optional.postTransform(type, options)receives the generated IR type.transformProperty(property, schemaObject, options)receives the generated IR property (name,type,isOptional,isReadonly,comments) and returns a new one.- Returning
undefinedkeeps the generated type or property.
import openapiTS, { astToString, NULL, tsUnion } from "openapi-typesafe";
const ast = await openapiTS(mySchema, {
irCallbacks: true,
transform(schemaObject) {
if (schemaObject.format === "date-time") {
return schemaObject.nullable ? "Date | null" : "Date";
}
},
postTransform(type, options) {
return options.path?.endsWith("/id") ? tsUnion([type, NULL]) : undefined;
},
transformProperty(property) {
return property.isOptional ? { ...property, isReadonly: true } : undefined;
},
});
const contents = astToString(ast);Building and reading IR
- To build a type, use the helpers and constants (
tsUnion,tsIntersection,tsLiteral,tsRecord,tsNullable,tsOmit,tsWithRequired,oapiRef,STRING,NUMBER,NULL,UNKNOWN, …) or return TypeScript type text as a string. These are the supported ways; the constructors inside the package (dist/domain/ir/…) are not part of the API. - To change a property or a type you receive, spread it and replace fields:
{ ...property, isReadonly: true }. - The node types (
IrType,IrPropertySignature,IrDeclaration, …) are exported for reading. A minor release may add a node kind, so aswitchoverkindneeds adefaultbranch that keeps an unknown node as it is (see Versioning). encodingon a string literal andoriginon a raw type are printer details: leaveencodingout (the printer writes JSON text) and never setorigin.
transform / postTransform
Use the transform() and postTransform() options to override the default Schema Object transformer with your own. This is useful for providing nonstandard modifications for specific parts of your schema.
transform()runs before the conversion to TypeScript (you’re working with the original OpenAPI nodes)postTransform()runs after the conversion to TypeScript (you’re working with the generated IR type)
Example: Date types
For example, say your schema has the following property:
properties:
updated_at:
type: string
format: date-timeBy default, openapiTS will generate updated_at?: string; because it’s not sure which format you want by "date-time" (formats are nonstandard and can be whatever you’d like). But we can enhance this by providing our own custom formatter, like so:
import openapiTS from "openapi-typesafe";
const ast = await openapiTS(mySchema, {
irCallbacks: true,
transform(schemaObject, metadata) {
if (schemaObject.format === "date-time") {
return schemaObject.nullable ? "Date | null" : "Date";
}
},
});That would result in the following change:
updated_at?: string;
updated_at?: Date; Example: Blob types
TIP
binaryAsBlob: true (--binary-as-blob) does this without a transform, also for OpenAPI 3.1 contentMediaType and nullable schemas. The example below still shows how a transform works.
Another common transformation is for file uploads, where the body of a request is a multipart/form-data with some Blob fields. Here's an example schema:
Body_file_upload:
type: object
properties:
file:
type: string
format: binaryUse the same pattern to transform the types:
import openapiTS from "openapi-typesafe";
const ast = await openapiTS(mySchema, {
irCallbacks: true,
transform(schemaObject, metadata) {
if (schemaObject.format === "binary") {
return schemaObject.nullable ? "Blob | null" : "Blob";
}
},
});Resultant diff with correctly-typed file property:
file?: string;
file?: Blob; Example: Add "?" token to property
It is not possible to create a property with the optional "?" token with the above transform functions. The transform function also accepts a different return object, which allows you to add a "?" token to a required property. Here's an example schema:
Body_file_upload:
type: object
required: [file]
properties:
file:
type: string
format: binaryHere we return an object with a schema property, which is the same as the above example, but we also add a questionToken property, which will add the "?" token to the property.
import openapiTS from "openapi-typesafe";
const ast = await openapiTS(mySchema, {
irCallbacks: true,
transform(schemaObject, metadata) {
if (schemaObject.format === "binary") {
return {
schema: schemaObject.nullable ? "Blob | null" : "Blob",
questionToken: true,
};
}
},
});Resultant diff with correctly-typed file property and "?" token:
file: string;
file?: Blob; Any Schema Object present in your schema will be run through this formatter (even remote ones!). Also be sure to check the metadata parameter for additional context that may be helpful.
There are many other uses for this besides checking format. Because it can return TypeScript type text, you can produce any type you'd like (even your own custom types).
transformProperty
Use the transformProperty() option to modify individual property signatures within Schema Objects. This is particularly useful for adding JSDoc comments, validation annotations, or modifying property-level attributes that can't be achieved with transform or postTransform.
transformProperty()runs after type conversion but before JSDoc comments are added- It receives the property (with
irCallbacks: true, an IR property:name,type,isOptional,isReadonly,comments), the original schema object, and transformation options - It returns a changed property, or
undefinedto leave the property unchanged
Example: JSDoc validation annotations
A common use case is adding validation annotations based on OpenAPI schema constraints:
components:
schemas:
User:
type: object
properties:
name:
type: string
minLength: 1
pattern: "^[a-zA-Z0-9]+$"
email:
type: string
format: email
age:
type: integer
minimum: 0
maximum: 120
required: [name, email]import fs from "node:fs";
import openapiTS, { astToString } from "openapi-typesafe";
const ast = await openapiTS(mySchema, {
irCallbacks: true,
transformProperty(property, schemaObject) {
// Add validation JSDoc tags based on schema constraints
const constraints = schemaObject as Record<string, unknown>;
const validationTags = ["minLength", "maxLength", "minimum", "maximum", "pattern", "format"]
.filter((key) => constraints[key] !== undefined)
.map((key) => `@${key} ${constraints[key]}`);
if (validationTags.length === 0) {
return undefined;
}
// A JSDoc block: the comment text goes between `/*` and `*/`
const text = `*\n * ${validationTags.join("\n * ")}\n `;
return { ...property, comments: [...property.comments, { kind: "block", text }] };
},
});
const contents = astToString(ast);
fs.writeFileSync("./my-schema.ts", contents);This transforms the schema into TypeScript with validation annotations:
export interface components {
schemas: {
User: {
/**
* @minLength 1
* @pattern ^[a-zA-Z0-9]+$
*/
name: string;
/**
* @format email
*/
email: string;
/**
* @minimum 0
* @maximum 120
*/
age?: number;
};
};
}The transformProperty function provides access to:
property: The generated IR property (withoutirCallbacks, a TypeScriptPropertySignaturenode; deprecated)schemaObject: The original OpenAPI Schema Object for this propertyoptions: Transformation context including path information and other utilities