Skip to content

Migrating from openapi-typescript ​

openapi-typesafe is a drop-in successor. A project on these packages moves by changing names:

BeforeAfter
openapi-typescript 7.x (package and command)openapi-typesafe
openapi-fetch 0.xopenapi-typesafe-fetch
openapi-react-queryopenapi-typesafe-react-query
openapi-typescript-helpersopenapi-typesafe-helpers

All openapi-typesafe packages have one version number. The generated types are the same (except for the changes below), the CLI flags are the same, and redocly.yaml keeps its x-openapi-ts settings.

Coming from openapi-typescript 6.x? Follow the upstream guide to 7.x first, then this one.

Coming from openapi-fetch older than 0.17 or openapi-react-query older than 0.5.4? Upgrade them upstream first (openapi-fetch@0.17, openapi-react-query@0.5.4), run your type check and tests, then follow this guide. openapi-typesafe-fetch continues openapi-fetch 0.17 and openapi-typesafe-react-query continues openapi-react-query 0.5.4, so the changes before those versions are upstream's and are listed in the openapi-fetch changelog and the openapi-react-query changelog. For example, openapi-fetch 0.14 dropped the minified build (dist/index.min.js), and openapi-react-query 0.5.4 needs @tanstack/react-query 5.80 or newer. The openapi-react-query@1.0.0 on npm is older than 0.5.1; upgrade it to 0.5.4 too.

1. Run the migration tool ​

In the project root, with a clean git tree:

bash
npx openapi-typesafe-migrate          # report what would change
npx openapi-typesafe-migrate --write  # change the files

It renames the packages in every package.json and in pnpm catalogs, the module specifiers in your sources, and the openapi-typescript command in package.json scripts, shell scripts, CI files, Makefiles and Dockerfiles. It also writes the new spelling of CLI flags that openapi-typescript renamed. A legacy transform callback that only returns ts.factory types becomes type text with irCallbacks: true.

It reads the files git lists, so files that .gitignore ignores (build output, for example) stay as they are. Submodules and nested repositories are read with their own .gitignore.

Whatever it cannot change safely, it lists with the file and line: version specs such as latest, overrides, renamed peer dependencies, npx openapi-typescript@<version>, deep imports, strings that equal a package name outside an import (an object key or a bundler option, for example), swr-openapi, and callbacks it cannot rewrite. Every finding also goes to a log file whose path it prints. See the tool’s README for details.

Prefer to do it by hand? Rename the packages in the table above in package.json, imports and scripts; nothing else is required.

2. Install and regenerate once ​

bash
npm install            # or pnpm install, yarn, bun install
npm run generate       # your script that runs openapi-typesafe

With Yarn 4 and enableImmutableInstalls (on by default in CI), this first install must change the lockfile: run yarn install --no-immutable once and commit yarn.lock.

Commit the regenerated files once. Unless your schemas have open string enums or one of the output fixes applies, the only changes are the comments that name the generator, first the header line:

diff
 /**
- * This file was auto-generated by openapi-typescript.
+ * This file was auto-generated by openapi-typesafe.
  * Do not make direct changes to the file.
  */

A schema with a discriminator also gets the generator's name in the comment of each added discriminator property: @description discriminator enum property added by openapi-typesafe.

If you check generated files in CI with --check, it fails until this commit lands, because the files on disk still carry the old header.

The run may print warnings that openapi-typescript did not print, while the types stay the same. Each one ends with a code, such as PATH_TEMPLATES_IDENTICAL for paths that differ only in their parameter names, or LINT_PROBLEM with the rule operation-operationId-unique for a repeated operationId. A warning does not change the output or the exit code; the list of warnings says what each one means.

3. Type check and test ​

Run your type check and tests. For the generator and the client, nothing else changes, with the exceptions below.

What is different ​

Node.js ​

openapi-typesafe needs Node.js 22.12 or newer (the client and helpers: Node.js 22). The CommonJS build loads ESM-only dependencies with require(), which Node.js supports without a flag from 22.12.

No TypeScript dependency ​

The generator does not need the typescript package. It prints types itself, so it runs next to TypeScript 7 (see TypeScript 7). typescript is an optional peer dependency, loaded only by the legacy callbacks below. Its range is >=5.4.0, the TypeScript versions openapi-typesafe supports; openapi-typescript said ^5.x. On TypeScript 5.0 to 5.3, npm reports a peer conflict (ERESOLVE): upgrade TypeScript to 5.4 or newer.

Node API returns IR ​

openapiTS() returns the declarations as IR objects (IrDeclaration[]), not TypeScript compiler nodes. astToString(await openapiTS(…)) works unchanged and prints the same text. Code that inspected or edited the returned ts.Nodes must use the IR objects instead. The helpers (tsUnion, NULL, STRING, …) and the exported transformers return IR too; they still accept TypeScript nodes as input. See Node.js API.

Legacy callbacks ​

transform, postTransform and transformProperty that work with TypeScript nodes keep working in 1.x, with a deprecation warning, as long as a TypeScript JavaScript API can be loaded: typescript 6 or earlier, or @typescript/typescript6 in a TypeScript 7 project. Move them to IR callbacks (irCallbacks: true), which return IR or plain type text such as "Date | null". The TypeScript-node forms are removed in 2.0.

swr-openapi ​

swr-openapi 5 has openapi-fetch (0.15 or newer) as a peer dependency and takes its client type from it. Alias openapi-fetch to openapi-typesafe-fetch with overrides or resolutions, as Libraries built on openapi-fetch shows; without the alias, passing it the openapi-typesafe-fetch client fails with TS2345. The tool's report reminds you of this when it finds swr-openapi.

Renamed CLI flags ​

As in openapi-typescript 7, the old spellings fail with an error that names the new one: --immutable-types and -it (use --immutable), --support-array-length (--array-length), --redoc (--redocly) and -ap (--additional-properties).

CLI exit codes and errors ​

Failures now exit with a documented code: 1 stale output with --check (as before), 2 usage or configuration, 3 input document, 4 output files, 70 unexpected error. Before, every failure exited with 1. See exit codes. --check without --output (and without apis in redocly.yaml) now fails with 2 and CLI_CHECK_OUTPUT_MISSING; before, it printed the types and passed without checking anything. Without a schema, with nothing piped into stdin (stdin is a terminal) and without apis in redocly.yaml, the CLI now fails with 2 and CLI_INPUT_MISSING; before, it waited for input with no prompt, which looked like a hang. An --output (or x-openapi-ts.output) that is a schema file the CLI reads now fails with 2 and CLI_OUTPUT_IS_INPUT and writes nothing; before, the types replaced the schema. APIs in redocly.yaml with the same x-openapi-ts.output now all fail with 2 and CLI_OUTPUT_DUPLICATE; before, the API that finished last silently won.

OpenAPI 3.2 documents get types for query and additionalOperations operations, itemSchema (an itemContent map next to content), in: querystring parameters and components.mediaTypes; openapi-typescript leaves them out. HttpMethod from openapi-typesafe-helpers includes "query", and the fetch client has QUERY(). A Record<HttpMethod, …> you write yourself needs a query entry. The path-based client no longer offers PARAMETERS(), which was typed but always failed. The types of these 3.2 additions are experimental in 1.x and may change in a minor release. See OpenAPI 3.2 support.

Errors thrown by the Node API are OpenAPITypesafeErrors with a stable code (see errors). The messages are unchanged. A file system error is kept as cause: check error.cause.code instead of error.code === "ENOENT".

Open string enums ​

A type: string enum with additionalProperties: true is typed with any other string added, so editors still suggest the values while other strings are accepted:

ts
status: ("active" | "inactive") | (string & {}); // openapi-typescript 7.13.0: "active" | "inactive"

This change was merged upstream after 7.13.0 was published (openapi-typescript#2415) and is on by default. It changes valid output, so check the types of such schemas after you regenerate.

Headers for remote documents ​

Headers from resolve.http.headers in redocly.yaml go over plain HTTP only to this machine (localhost, 127.0.0.0/8, ::1) or when the pattern starts with http://. A header for a plain HTTP server on another machine needs its pattern written as http://api.internal:8080/**. Headers are no longer sent on through a redirect to another origin.

Output fixes ​

Where openapi-typescript’s output did not compile, was unsafe or had wrong types, it changes:

  • --enum and --enum-values output that did not compile now does.
  • --root-types with --enum: a root type alias that had the same name as an enum gets a suffix (ClusterState_2).
  • --root-types: a root type alias that had the same name as a helper type of the file (a schema named WithRequired with --root-types-no-schema-prefix) gets the same suffix (WithRequired_2).
  • --root-types names that were not identifiers now are: --root-types-keep-casing writes characters such as - as _ (SchemaAddress_3), and --root-types-no-schema-prefix keeps the prefix before a digit (Schema1) and before a name that TypeScript reserves, which only --root-types-keep-casing can give (Schemavoid, not void).
  • Names and URLs from the document are escaped where they reach code (--root-types, --path-params-as-types), so a document can no longer add code to the generated file.
  • The inject option is printed as written, so comments in it are kept.
  • --make-paths-enum: ApiPaths members whose names collided (GetUser for both /user and /user/{user_id}) did not compile. Colliding names built from the URL now name the path parameters (GetUserByUserId); names that still collide, such as a repeated operationId, get a number (listItems2). Names that did not collide stay the same.
  • --enum and --make-paths-enum: values whose member names come out the same after invalid characters are replaced (reactions-+1 and reactions--1 are both reactions_1) did not compile. A repeated name gets a number: reactions_1_2 (a quoted name gets it inside the quotes). Names that did not collide stay the same.
  • --exclude-deprecated: a deprecated schema that kept output still references (an array's items, a kept operation's response) was left out, so the output named a missing components["schemas"] key. It is kept now; a deprecated schema that only dropped parts reference is still left out.
  • --path-params-as-types: a template key such as `/apps/${string}` also matches /apps/regions and `/apps/${string}/restart`, and that output did not compile (TS2411, TS2413) when their types do not fit the template's. When a path surely does not fit (a method or a parameter location one has and the other has not, or an operation or response schema that lacks a required property of the template's), paths is now an intersection type alias, { … } & { [path: `/apps/${string}`]: … }, and every path keeps its type. Output that compiled stays the same, export interface paths included.
  • JSON Schema $defs (OpenAPI 3.1): a schema's $defs member is optional ($defs?: { … }), and a $ref into it reads NonNullable<components["schemas"]["Foo"]["$defs"]>["bar"]. The root export type $defs has one member per subschema; before, it was Record<string, never> and #/$defs/<name> refs were never.
  • A schema that allOfs a discriminated union which lists that schema (Cat: { allOf: [Pet] }, Pet: { oneOf: [Cat, Dog] }) referred to itself through Omit<Pet> and did not compile (TS2502, upstream #1665). It is now typed with the keywords of Pet itself, without the union. When Pet inherits the discriminator through allOf, neither Pet nor its keywords get the value "Pet", which every member would contradict.

All other output stays the same. The compatibility report checks this on 1,820 real-world documents with 28 option sets (50,960 cases): 99.99% of the outputs are the same byte for byte and 100% have the same types. Its golden output is the 7.13 code at tag fork-base, which already has the open string enums change.

New options ​

These are off by default, so the output does not change until you turn one on.

  • --binary-as-blob (binaryAsBlob): raw binary strings (format: binary, or a contentMediaType that is not text) become Blob instead of string. It replaces the Blobtransform recipe; the names follow openapi-typescript PR #2878.

  • --fix (fix): opt-in fixes of the output of valid schemas, by name or all. They become the default in 2.0. The first one, allof-required, makes the keys of an allOf member with only required required instead of adding Record<string, never>. anyof-oneof-required makes an anyOf or oneOf of required-only variants the parent with each variant's keys required, instead of unknown. anyof-properties intersects anyOf with its sibling properties instead of adding them to the union. array-length-tuples corrects the --array-length tuples (minItems to maxItems items, nested arrays, readonly). discriminator-values gives a schema that a discriminator mapping names one enum with the mapped values, instead of also adding its schema name, which made the property never. root-type-numbering (with --root-types --root-types-no-schema-prefix) gives each schema of one root name an alias: the later keeps the name, the earlier get _2, _3, instead of none. unevaluated-properties types the keys that no other keyword declares with the unevaluatedProperties schema, instead of ignoring it. discriminator-values also keeps a schema's own mapping when it inherits a discriminator, adds no second property to a member that declares its value, and with --enum types the value as a member of the owner's enum (PetType.Cat). allof-discriminated-union types an allOf $ref to a discriminated union as the union, without Omit or an invented value. discriminator-member-required keeps WithRequired next to an allOf $ref to a discriminator member. anyof-oneof-descriptions lists the descriptions of anyOf and oneOf members in the JSDoc. dedupe-enum-names (with --enum --dedupe-enums) names a shared enum by a rule that does not depend on the document order. exploded-query-objects makes the properties of an exploded object query parameter keys of query, as they are sent. nullable-enums (with --enum) declares an enum that lists null and adds | null to its type. path-param-type-names (with --path-params-as-types) gives a path parameter named number, string or boolean its own placeholder. prefix-items makes a prefixItems tuple open, with the elements after minItems optional. read-write-refs makes a property readonly when the schema it references is readOnly. ref-pointers gives a $ref into another schema's keywords the type it points at also where an index signature made the original compile as unknown.

Libraries built on openapi-fetch ​

A library that imports types from openapi-fetch, such as swr-openapi, needs openapi-fetch aliased to openapi-typesafe-fetch with overrides (npm, bun, pnpm-workspace.yaml) or resolutions (yarn). See Libraries built on openapi-fetch.

Client fixes ​

  • A string body with a non-JSON Content-Type such as text/plain is sent as is, not JSON-encoded.
  • A path parameter that makes a . or .. segment is rejected before the request is sent.
  • openapi-typesafe-react-query fails on every non-OK response, also one without a body.
  • Middleware request ids come from a counter instead of Math.random().
  • A path that starts with // once the path parameters are in (an empty first parameter) is rejected before the request is sent, since a browser sends it to another host.
  • An error response with an empty body gives error: undefined, also without Content-Length; it gave "".
  • openapi-typesafe-react-query: useInfiniteQuery has its own query key, [...queryKey, "infinite"], so it no longer shares a cache entry with useQuery. [method, path] still matches it as a prefix; code that reads or writes the entry with the exact key must append "infinite".
  • A Blob, File, ArrayBuffer, typed array or URLSearchParams body is sent as is; before, it was JSON-encoded to {}.
  • A nested object in an application/x-www-form-urlencoded body throws instead of being sent as [object Object]; write a bodySerializer for the format your server reads.
  • An object body with Content-Type: multipart/form-data is sent as FormData (one part per field), not as JSON, and a multipart/form-data header without a boundary no longer breaks a FormData upload.
  • A path that already has a query string gets & before params.query, not a second ?.
  • defaultBodySerializer declares the type it returns instead of string: a raw body as its own type, undefined for undefined, and FormData or string | FormData for an object with a multipart or an unknown Content-Type. An object without headers, or with headers written as a record, is still string unless the Content-Type is multipart. Code that used the result as a string in the other cases, or in a global bodySerializer (its body is typed undefined), no longer compiles: check typeof result === "string".
  • A Date parameter is sent as ISO 8601 (toISOString()) in every query and path style. Before, a Date on its own or in a path was left out (it is an object without members), and one in an array or a form object was sent as String(date), a local time text.
  • A query parameter that serializes to nothing (an empty array or object, null) no longer leaves a stray & (a=1&&b=2), and null items of exploded arrays and objects are left out too.
  • A success body that is not JSON (for example a proxy's HTML page) throws a SyntaxError that names the status, the Content-Type and the start of the body and says which parseAs to set (parseAs: "text" for a text/* response), with the parser's error as cause; before, it was the parser's bare SyntaxError. Bodies are still parsed as JSON unless parseAs is set, as in openapi-fetch. See response body.
  • TRACE() is unchanged, and its platform differences are documented: browsers, Node.js and Deno reject it, Bun sends it and returns an empty body. See TRACE.
  • Readable and Writable keep function types, so a Blob or Date in response data keeps its methods (data.file.arrayBuffer()).

The full list, with the upstream issues each change closes, is in the changelog.

Released under the MIT License. Built on openapi-typescript, openapi-fetch and openapi-react-query.