---
url: https://openapi-typesafe.github.io/openapi-typesafe/migration-guide.md
description: >-
  Move a project from openapi-typescript, openapi-fetch and openapi-react-query
  to openapi-typesafe
---

# Migrating from openapi-typescript

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

| Before | After |
| :-- | :-- |
| `openapi-typescript` 7.x (package and command) | `openapi-typesafe` |
| `openapi-fetch` 0.x | `openapi-typesafe-fetch` |
| `openapi-react-query` | `openapi-typesafe-react-query` |
| `openapi-typescript-helpers` | `openapi-typesafe-helpers` |

All openapi-typesafe packages have one version number. The generated types are the same (except for the [changes below](#what-is-different)), 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](https://openapi-ts.dev/migration-guide) 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](https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-fetch/CHANGELOG.md) and the [openapi-react-query changelog](https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-react-query/CHANGELOG.md). 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](#renamed-cli-flags). A legacy [`transform` callback](#legacy-callbacks) 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](#swr-openapi), and callbacks it cannot rewrite. Every finding also goes to a log file whose path it prints. See the [tool’s README](https://github.com/openapi-typesafe/openapi-typesafe/tree/main/packages/openapi-typesafe-migrate) 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](#open-string-enums) or one of the [output fixes](#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](./cli.md#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-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.Node`s 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](/node).

### 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](/node#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](https://github.com/htunnicliff/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](/fetch/#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](/cli#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](/introduction#openapi-3-2-support).

Errors thrown by the Node API are `OpenAPITypesafeError`s with a stable `code` (see [errors](/node#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](https://github.com/openapi-ts/openapi-typescript/pull/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 `allOf`s 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](./compatibility.md) 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](#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`](/cli#binaryasblob) (`binaryAsBlob`): raw binary strings (`format: binary`, or
  a `contentMediaType` that is not text) become `Blob` instead of `string`. It replaces the `Blob`
  `transform` recipe; the names follow openapi-typescript PR #2878.

* [`--fix`](/cli#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](/fetch/#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](/fetch/api#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](/fetch/api#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](https://github.com/openapi-typesafe/openapi-typesafe/blob/main/packages/openapi-typesafe/CHANGELOG.md).
