# How to rename a generated SDK method without editing the output

> Rename a generated method where its name is decided, in the OpenAPI document or in the generator, regenerate, and prove the rename reached every target.

Source: https://voxgig.com/howto/rename-generated-methods-without-editing-output

- Audience: api-producer
- Level: intermediate
- Languages: typescript, javascript
- Verified: 2026-10-21
- Published: 2026-10-18

## Short answer

Fix the name where it is decided, then regenerate. When the operationId is the problem, change it in an OpenAPI Overlay applied before generation, so every tool that reads the document gets the replacement. When the document is not yours, use the generator's own mapping: OpenAPI Generator's `--operation-id-name-mappings`, Speakeasy's `x-speakeasy-name-override`, or a correction in sdkgen's guide file. Then grep the output for both names and compile a caller.

---
## You will need

Node 22 or later, an OpenAPI 3 document, and a generated client with awkward method names.
Verified 2026-10-21 against Node 22.22.0, `@hey-api/openapi-ts` 0.99.0,
`openapi-typescript` 7.13.0, `openapi-format` 1.34.0, and `@voxgig/apidef` 8.25.3. The sample
generates a TypeScript client three ways from a five-operation document, applies an overlay, and
compiles two callers, offline.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [OpenAPI Generator name mappings](https://openapi-generator.tech/docs/customization/) | You run OpenAPI Generator and cannot change the document it reads | A mapping repeated on every command line and in every language's run, with no validation of the names it writes | You own the document, where one change reaches every generator |
| [OpenAPI Overlay](https://spec.openapis.org/overlay/v1.0.0.html) | The operationId is wrong and you can put a reviewed file in front of generation | A second file to keep in step with the document, whose JSONPath targets go stale when a path moves | The generator ignores operationIds, as path-driven generators do |
| [sdkgen model overrides](https://voxgig.com/sdk/custom) | The client is generated by sdkgen and the entity name, not the operationId, is what reads badly | A correction in a guide file that only the sdkgen model step reads, so other generators see nothing of it | Your generator names methods from the operationId |
| [Speakeasy x-speakeasy-name-override](https://www.speakeasy.com/docs/sdks/customize/methods) | Speakeasy generates the SDK and you can add a vendor extension to the document | An extension other generators ignore, and a global form that renames by regular expression over operationIds | You cannot add vendor extensions to the published document |

Voxgig maintains sdkgen. It is one of four options here, not the recommendation.

The overlay is the one lever that reaches every generator reading operationIds, at the price of a
file whose targets break when a path moves. The other three are local, which also makes them
reversible. OpenAPI Generator's mapping is typed into each language's command, Speakeasy's
extension sits in a document you may not own, and sdkgen's correction is read by its model step
alone.

## Read what each generator made of the ids

Three generators read one document. [`@hey-api/openapi-ts`](https://heyapi.dev/docs/openapi/typescript/plugins/sdk)
exports one function per operation, named from the operationId.
[`openapi-typescript`](https://openapi-ts.dev/introduction) keys its `operations` interface the
same way. `@voxgig/apidef` is the model step inside sdkgen. It ignores the id and classifies paths
into entities and operations, so its names read `entity.op`. `names.mjs` runs all three.

```bash
node names.mjs api.json
```

```text output
api.json: 5 operations
  method path                   @hey-api/openapi-ts 0.99.0   openapi-typescript 7.13.0  @voxgig/apidef 8.25.3
  GET    /v2/whs/{wh}           getWarehouse                 getWarehouse               warehouse.load
  GET    /v2/whs/{wh}/shpts     listGetV2ByWh                listGetV2ByWh              shpt.list
  POST   /v2/whs/{wh}/shpts     postShptV2                   postShptV2                 shpt.create
  GET    /v2/shpts/{shpt}       getShptById                  getShptById                shpt.load
  DELETE /v2/shpts/{shpt}       deleteShptById               deleteShptById             shipment.remove
```

The id-driven columns repeat the document's ids. The path-driven column is a different kind of
answer: `whs` came out as `warehouse`, `shpts` as `shpt`, and the DELETE
landed on a second entity, `shipment`. The awkward id never reaches that column. An operationId
rename cannot improve sdkgen's output, and renaming the path would change the API, so its model
is the place to fix it.

## Rename the ids in an overlay and regenerate

An [OpenAPI Overlay](https://spec.openapis.org/overlay/v1.0.0.html) is a list of actions applied
to a document before anything reads it. Each action has a `target`, a JSONPath as
[RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html) defines it, and an `update` merged into
whatever the target selects.[^1] The document stays as exported, and the overlay re-applies to
every export.

```json title="rename.overlay.json"
{
  "overlay": "1.0.0",
  "info": { "title": "Depot API method names", "version": "1.0.0" },
  "actions": [
    {
      "target": "$.paths['/v2/whs/{wh}/shpts'].get",
      "description": "a router default that reached the published document",
      "update": { "operationId": "listShipments" }
    },
```

[`openapi-format`](https://github.com/thim81/openapi-format) applies it with `--overlayFile`;
`--no-sort` keeps the paths in order, so the two documents differ in the four ids alone.

```bash
npx openapi-format api.json --no-sort --overlayFile rename.overlay.json -o api.renamed.json && node names.mjs api.renamed.json
```

```text output
api.renamed.json: 5 operations
  method path                   @hey-api/openapi-ts 0.99.0   openapi-typescript 7.13.0  @voxgig/apidef 8.25.3
  GET    /v2/whs/{wh}           getWarehouse                 getWarehouse               warehouse.load
  GET    /v2/whs/{wh}/shpts     listShipments                listShipments              shpt.list
  POST   /v2/whs/{wh}/shpts     createShipment               createShipment             shpt.create
  GET    /v2/shpts/{shpt}       getShipment                  getShipment                shpt.load
  DELETE /v2/shpts/{shpt}       deleteShipment               deleteShipment             shipment.remove
```

Both id-driven generators moved, because both read the id; the path-driven column stayed, because
the paths stayed. A rename in the document reaches every tool that reads it: these two
generators, a docs site, a run-time CLI, and the next generator you adopt.

## Map the name inside one generator when the document is not yours

A generator-level mapping renames in one tool's output. OpenAPI Generator takes
`--operation-id-name-mappings getPetById=returnPetById`, and its
[customization page](https://openapi-generator.tech/docs/customization/) warns that its mapping
options validate nothing they are given.[^2] Speakeasy reads `x-speakeasy-name-override` on an operation, or
a global list of operationId regular expressions at the document root.[^3] `@hey-api/openapi-ts`
exposes the lever as a function: the sdk plugin's `operations.nesting` hook returns the path of
each generated function, and a one-segment path is a flat name.

```js title="hook.mjs"
export const renames = {
  listGetV2ByWh: 'listShipments',
  postShptV2: 'createShipment',
  getShptById: 'getShipment',
  deleteShptById: 'deleteShipment',
}

export const nesting = (operation) => [renames[operation.operationId] ?? operation.operationId]
```

```bash
node names.mjs api.json --hook
```

```text output
api.json: 5 operations (hey-api with hook.mjs)
  method path                   @hey-api/openapi-ts 0.99.0   openapi-typescript 7.13.0  @voxgig/apidef 8.25.3
  GET    /v2/whs/{wh}           getWarehouse                 getWarehouse               warehouse.load
  GET    /v2/whs/{wh}/shpts     listShipments                listGetV2ByWh              shpt.list
  POST   /v2/whs/{wh}/shpts     createShipment               postShptV2                 shpt.create
  GET    /v2/shpts/{shpt}       getShipment                  getShptById                shpt.load
  DELETE /v2/shpts/{shpt}       deleteShipment               deleteShptById             shipment.remove
```

Only the `@hey-api/openapi-ts` column moved, which is the cost of a generator-level mapping: it
is repeated in each tool. The hook has a side effect too. With a
`nesting` function in place, the generator names each type from the method and path rather than
the id, so `ListGetV2ByWhData` becomes `GetV2WhsByWhShptsData`.

sdkgen's lever is the guide. The model step writes its guessed classification to
a base guide on every run. Corrections go in the entry file beside it,
`.sdk/model/guide/guide.aontu` in a generated project, which regeneration never rewrites. The
[guide reference](https://github.com/voxgig/apidef/blob/main/docs/reference/guide.md) lists the
shapes a correction can take. This one gives the `shpts` paths to an entity named `shipment` and
switches the guessed entity off.

```aontu title="depot-guide.aontu"
@"@voxgig/apidef/model/guide.aontu"
@"./depot-base-guide.aontu"

# The heuristic read /v2/shpts as an entity called shpt. Give the same paths
# to an entity called shipment, and switch the guessed name off.
guide: entity: shipment: path: "/v2/whs/{wh}/shpts": { op: list: method: GET, op: create: method: POST }
guide: entity: shipment: path: "/v2/shpts/{shpt}": { rename: param: shpt: id, op: load: method: GET, op: remove: method: DELETE }
guide: entity: shpt: active: false
```

```bash
node names.mjs api.json --guide depot-guide.aontu
```

```text output
api.json: 5 operations (apidef with depot-guide.aontu)
  method path                   @hey-api/openapi-ts 0.99.0   openapi-typescript 7.13.0  @voxgig/apidef 8.25.3
  GET    /v2/whs/{wh}           getWarehouse                 getWarehouse               warehouse.load
  GET    /v2/whs/{wh}/shpts     listGetV2ByWh                listGetV2ByWh              shipment.list
  POST   /v2/whs/{wh}/shpts     postShptV2                   postShptV2                 shipment.create
  GET    /v2/shpts/{shpt}       getShptById                  getShptById                shipment.load
  DELETE /v2/shpts/{shpt}       deleteShptById               deleteShptById             shipment.remove
```

Every shipment operation sits on one entity with the name you chose, and the id-driven columns
are untouched, because only apidef reads a guide. In an sdkgen project every target is generated
from that model, so the correction reaches the Go and Python clients too.

## Keep the former names for one release

A published method name is public API, and [semantic versioning](https://semver.org/spec/v2.0.0.html)
reserves its removal for a major version. Inside a minor release, keep the former name as a
deprecated alias, say when it goes, and remove it at the next major. The aliases belong outside
the generated tree, and their list has one source, the overlay. `alias.mjs` reads each action's
target and update, looks up the id it replaced, and writes `compat.gen.ts`, with aliases for the
types named after the id too. A target it cannot resolve stops it, since a skipped action ships a
rename with no alias.

```js title="alias.mjs"
// @hey-api/openapi-ts names an operation's types after its id too, so the
// rename removes ListGetV2ByWhData and its siblings along with the function.
const TYPE_SUFFIXES = ['Data', 'Errors', 'Error', 'Responses', 'Response']
const pascal = (id) => id[0].toUpperCase() + id.slice(1)

/** A TypeScript module exporting each old name, and its types, as deprecated aliases of the new ones. */
export function compatModule(pairs, removedIn, generatedTypes) {
  const types = pairs.flatMap(({ from, to }) =>
    TYPE_SUFFIXES.filter((s) => generatedTypes.includes(pascal(to) + s)).map((s) => ({ from: pascal(from) + s, to: pascal(to) + s })),
  )
  const lines = [
    '// Generated by alias.mjs from rename.overlay.json: the old names, kept for one release.',
    `import { ${pairs.map((p) => p.to).join(', ')} } from './sdk/sdk.gen'`,
    `import type { ${types.map((p) => p.to).join(', ')} } from './sdk/types.gen'`,
    '',
  ]
  for (const { from, to } of pairs) {
    lines.push(`/** @deprecated Renamed to ${to}. This alias is removed in ${removedIn}. */`)
    lines.push(`export const ${from} = ${to}`)
  }
  for (const { from, to } of types) {
    lines.push(`/** @deprecated Renamed to ${to}. This alias is removed in ${removedIn}. */`)
    lines.push(`export type ${from} = ${to}`)
  }
  return lines.join('\n') + '\n'
}
```

`index.ts`, the package entry, is the other hand-owned file. It re-exports the generated `sdk/`
tree and `compat.gen.ts`, so the package exposes both sets of names.

```ts title="index.ts"
export * from './sdk/index'
export * from './compat.gen'
```

`compile.mjs` regenerates from the renamed document, counts whole-word mentions of each name in
the output, lays out the entry and the aliases, and runs `tsc --noEmit` over two callers. One
uses the new names, the other the names the published SDK had.

```bash
node compile.mjs
```

```text output
sdk/sdk.gen.ts from api.renamed.json, whole-word mentions of each name
  listShipments    1   listGetV2ByWh    0
  createShipment   1   postShptV2       0
  getShipment      1   getShptById      0
  deleteShipment   1   deleteShptById   0
compat.gen.ts: 4 functions and 12 types as deprecated aliases, re-exported by index.ts beside sdk/
tsc caller.ts, the new names: ok
tsc caller-old.ts, the old names: ok
```

The grep reads both ways: each replacement name appears once and each former name zero
times. TypeScript's [`@deprecated`](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html)
tag reaches editors as a strike-through and a suggestion diagnostic, and `tsc` never fails on
it.[^4] A Go target does the same with a `Deprecated:`
paragraph in its doc comment, as [Go's comment conventions](https://go.dev/doc/comment) define.

## Check it worked

Twelve tests run.

```bash
node --test rename.test.mjs
```

```text output
1..12
# tests 12
# suites 0
# pass 12
# fail 0
# cancelled 0
# skipped 0
# todo 0
```

Three matter most. One regenerates `api.renamed.json` from the committed overlay and compares it
with the committed copy, so the rename cannot drift from its record. Another compiles
`caller-old.ts` with and without `compat.gen.ts` and expects `TS2305` only without it. The third
imports every name the published package exported into the renamed one and expects no error.

## When it goes wrong

The rename ships as a minor release and a caller's build breaks on the upgrade. The symptom is
`TS2305`. The cause is a rename with no alias. The fix is `compat.gen.ts`, published in the same
release as the rename, with a removal version in every `@deprecated` message.

The tool reports success even though an overlay action matched nothing. The symptom is the awkward name
still in the regenerated client. The cause is a JSONPath that selects zero nodes, which the
Overlay specification allows and `openapi-format` accepts with exit 0. The fix is the grep:
regenerate and assert on both names. `pitfall.mjs` runs both failures.

```bash
node pitfall.mjs
```

```text output
the renamed SDK ships, and the package entry exports sdk/ alone
  caller-old.ts(2,10): error TS2305: Module '"./index"' has no exported member 'getShptById'.
  caller-old.ts(2,23): error TS2305: Module '"./index"' has no exported member 'listGetV2ByWh'.
  caller-old.ts(2,43): error TS2305: Module '"./index"' has no exported member 'ListGetV2ByWhResponse'.
  tsc exit 2

an overlay action whose target matches nothing
  exit 0, "OpenAPI formatted successfully"
  with --verbose: Applied actions: 0, Unused actions: 1
  GET /v2/whs/{wh}/shpts is still listGetV2ByWh
```

Only `--verbose` makes `openapi-format` report an unused action. Without it, the exit code and
the banner both report success. The third failure raises no error: a generator-level mapping
fixed the TypeScript client, but the Python client came from another tool reading the same
document, so the awkward name shipped there. Grep every target's output.

## When not to do this

Do not rename behind a published minor release without the alias. OpenAPI Generator and Speakeasy
make the method name the operationId, so a renamed id is a removed method. The caller finds out
at compile time or, in a dynamic language, in production.

Do not use a generator mapping as the permanent fix for a document you own. It is repeated for
every generator, a docs site or run-time CLI reading the document never sees it, and the next
tool you adopt starts from the awkward id. Fix the id in the overlay and ship the overlay with the
document.

Do not expect an operationId change to move sdkgen's output. Its model step names entities from
paths and operations from methods, so the rename changed nothing in its column. The guide is the
place for its names; renaming the path to get there changes the API itself.

Do not let `compat.gen.ts` outlive its deprecation window; a file nobody deletes is a second API
surface.

## Related how-tos

- [Write operationIds that generate clean SDK method names](/howto/operationids-that-generate-clean-sdk-method-names)

- [Fail a build when a generator customization becomes a fork](/howto/audit-generator-customisations-for-drift)

- [Generate your first SDK with sdkgen](/howto/first-sdk-with-sdkgen)

- [Preview a regeneration as a diff before writing files](/howto/preview-a-regeneration-as-a-diff)

## Last verified

Verified 2026-10-21 against Node 22.22.0, `@hey-api/openapi-ts` 0.99.0, `openapi-typescript`
7.13.0, `openapi-format` 1.34.0, `@voxgig/apidef` 8.25.3, and TypeScript 5.9.3. Every output
block is what the command preceding it printed. OpenAPI Generator and Speakeasy were not run;
their options come from their documentation. sdkgen itself was not run, because its
release on npm requires Node 24. The model step ran through `@voxgig/apidef` as a library with
the guide file shown, and the file's place in a project comes from sdkgen's documentation.

[^1]: The [Overlay Specification 1.0.0](https://spec.openapis.org/overlay/v1.0.0.html) was
published on October 17, 2024. Its `target` is a JSONPath as
[RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html) defines it, a standard eight months old
at the time. The specification requires the result to be zero or more objects or arrays and says
nothing further about zero, so an action that selects no node is a conforming action.

[^2]: OpenAPI Generator's [customization page](https://openapi-generator.tech/docs/customization/)
describes four name mappings, says that not all generators support these features yet, and lists
the pull requests that added them. The operation-id mapping is introduced below that list, so
whether the warning covers it is for the reader to find out.

[^3]: Speakeasy's [method customization page](https://www.speakeasy.com/docs/sdks/customize/methods)
gives the global form of `x-speakeasy-name-override` as a list at the document root, each entry
pairing an `operationId` regular expression with a `methodNameOverride`. Its example maps
`^get.*` to `get` and `^post.*` to `create`. A rename written as a regular expression also
renames every operation added later whose id happens to match.

[^4]: Neither convention is a compiler error. TypeScript's handbook describes
[`@deprecated`](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) as
information surfaced in completion lists and as a suggestion diagnostic that editors can handle
specially. Go's [doc comment conventions](https://go.dev/doc/comment)
treat a paragraph starting `Deprecated:` as a deprecation notice, say that some tools will warn
when deprecated identifiers are used, and that `pkg.go.dev` hides their documentation by default.