Your TypeScript client exports listGetV2ByWh. The name is a router default that reached the
published OpenAPI document, and the generator copied it without comment. Editing sdk.gen.ts
lasts until the next regeneration overwrites the edit. The Go client built from the same
document ships the same name untouched.
What you get
You will end up with a rename made where the name is decided, a module of deprecated aliases that keeps the former names for one release, and a test for both. This is for you if you regenerate a client you may not edit.
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 | 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 | 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 | 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 | 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
exports one function per operation, named from the operationId.
openapi-typescript 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.
node names.mjs api.json
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 is a list of actions applied
to a document before anything reads it. Each action has a target, a JSONPath as
RFC 9535 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.
{
"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 applies it with --overlayFile;
--no-sort keeps the paths in order, so the two documents differ in the four ids alone.
npx openapi-format api.json --no-sort --overlayFile rename.overlay.json -o api.renamed.json && node names.mjs api.renamed.json
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 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.
export const renames = {
listGetV2ByWh: 'listShipments',
postShptV2: 'createShipment',
getShptById: 'getShipment',
deleteShptById: 'deleteShipment',
}
export const nesting = (operation) => [renames[operation.operationId] ?? operation.operationId]
node names.mjs api.json --hook
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 lists the
shapes a correction can take. This one gives the shpts paths to an entity named shipment and
switches the guessed entity off.
@"@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
node names.mjs api.json --guide depot-guide.aontu
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
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.
// @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.
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.
node compile.mjs
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
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 define.
Check it worked
Twelve tests run.
node --test rename.test.mjs
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.
node pitfall.mjs
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
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.
Footnotes
-
The Overlay Specification 1.0.0 was published on October 17, 2024. Its
targetis a JSONPath as RFC 9535 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. ↩︎ Back to text -
OpenAPI Generator’s customization page 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. ↩︎ Back to text
-
Speakeasy’s method customization page gives the global form of
x-speakeasy-name-overrideas a list at the document root, each entry pairing anoperationIdregular expression with amethodNameOverride. Its example maps^get.*togetand^post.*tocreate. A rename written as a regular expression also renames every operation added later whose id happens to match. ↩︎ Back to text -
Neither convention is a compiler error. TypeScript’s handbook describes
@deprecatedas information surfaced in completion lists and as a suggestion diagnostic that editors can handle specially. Go’s doc comment conventions treat a paragraph startingDeprecated:as a deprecation notice, say that some tools will warn when deprecated identifiers are used, and thatpkg.go.devhides their documentation by default. ↩︎ Back to text