How-to › Ship an SDK

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.

Audience
API producer
Level
intermediate
Topic
Customize a generator without forking
Languages
TypeScript and JavaScript
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
OpenAPI Generator name mappingsYou run OpenAPI Generator and cannot change the document it readsA mapping repeated on every command line and in every language’s run, with no validation of the names it writesYou own the document, where one change reaches every generator
OpenAPI OverlayThe operationId is wrong and you can put a reviewed file in front of generationA second file to keep in step with the document, whose JSONPath targets go stale when a path movesThe generator ignores operationIds, as path-driven generators do
sdkgen model overridesThe client is generated by sdkgen and the entity name, not the operationId, is what reads badlyA correction in a guide file that only the sdkgen model step reads, so other generators see nothing of itYour generator names methods from the operationId
Speakeasy x-speakeasy-name-overrideSpeakeasy generates the SDK and you can add a vendor extension to the documentAn extension other generators ignore, and a global form that renames by regular expression over operationIdsYou 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.

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

  1. The Overlay Specification 1.0.0 was published on October 17, 2024. Its target is 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

  2. 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

  3. Speakeasy’s method customization page 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. ↩︎ Back to text

  4. Neither convention is a compiler error. TypeScript’s handbook describes @deprecated as information surfaced in completion lists and as a suggestion diagnostic that editors can handle specially. Go’s doc comment conventions 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. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.