operationId and tags in OpenAPI: naming conventions that keep generated SDKs and docs usable
Open a generated SDK where the methods are named getV1UsersByIdGet, postV1UsersPost, and usersGet2, and the cost of careless operationIds is immediate: nobody can discover anything, and every rename is a breaking change for downstream code. Open the docs sidebar and see 200 operations in one flat list because every endpoint got its own tag, and the same problem appears on the human side. These two fields look like optional labels; they are the public naming API of your service.
What each field actually drives
| Field | Consumed by | Consequence of getting it wrong |
|---|---|---|
operationId | Code generators, mocking tools, agent tool lists | Becomes the method/function name; must be unique and stable |
tags | Documentation navigation, generator namespaces | Groups operations into sections or SDK classes |
summary / description | Docs, AI tool descriptions | The sentence a developer or agent reads first |
x-* group names | Some renderers | Vendor-specific grouping; do not rely on it portably |
Generators that do not find an operationId synthesize one from the method and path, which is how you get getV1UsersByIdGet. Once a synthesized name ships, fixing it later breaks every caller. Set the id deliberately from day one.
operationId rules
Unique across the whole document. Two operations cannot share an id; some generators dedupe by appending a number, silently. Enforce uniqueness with a linter.
Language-neutral and code-safe. Use lowerCamelCase with ASCII letters and digits, starting with a letter. Avoid hyphens, dots, and spaces, because not every language maps them cleanly into an identifier.
Verb-first, resource-oriented, specific. The id should read as an action on a resource and say enough to be unambiguous without the path:
| Method and path | Avoid | Prefer |
|---|---|---|
| GET /users | getUsersGet | listUsers |
| POST /users | postUsers | createUser |
| GET /users/{id} | getUserById (fine) or getV1UsersId | getUser |
| PATCH /users/{id} | updateUser (ambiguous vs PUT) | patchUser / updateUser split by verb |
| DELETE /users/{id} | deleteUsersId | deleteUser |
| POST /users/{id}/archive | archive | archiveUser |
| GET /users/{id}/orders | getOrders | listUserOrders |
A consistent verb vocabulary removes the guesswork: list for collections, get for one, create, update/patch, delete, plus domain actions like archive, cancel, approve, export. Avoid generic verbs like process or handle that say nothing.
Stable forever once published. The id is part of the contract because generated code embeds it. Renaming it is a breaking change for SDK users even if the URL does not change. Treat it like a field name: pick it once, lint it, and never reuse a retired id for a different operation.
Do not encode the version or HTTP method. getV2Users bakes the version into the method, so a future v3 forces a rename even for clients that never cared. Version belongs in the server URL or path, not the id.
tags: a small, curated taxonomy
Tags group operations in docs and often become SDK classes or namespaces. The failure modes are a tag per endpoint (no grouping at all) and a single tag for everything (one giant list). Aim for a small, stable set aligned to business domains, not to URLs:
tags:
- name: Users
description: Customer accounts, profiles, and authentication identities.
- name: Orders
description: Order lifecycle, line items, and status transitions.
- name: Billing
description: Invoices, payment methods, and refunds.
- name: Webhooks
description: Event subscriptions and delivery logs.Rules that keep the taxonomy usable:
- Declare tags once at the root with a
description; do not rely on ad-hoc names appearing only on operations (typos then create duplicate tags likeUserandUsers). - Give each operation one primary tag for navigation. A second tag is occasionally useful for cross-cutting groups like
Webhooks, but three or more tags per operation scatter it across the docs. - Name tags as domains a customer recognizes (
Billing,Orders), not internal service names (payment-svc) or HTTP concepts. - Keep the set stable; renaming a tag rearranges every generated SDK namespace and the docs sidebar.
If you need finer grouping than a tag gives, use x-tagGroups (supported by several renderers) to cluster tags into sections like "Catalog" and "Account" without multiplying tags.
summary and description help both humans and agents
summary is a short imperative label; description is the detail. For an AI agent that turns operations into callable tools, the summary and the first line of the description are often the entire basis for choosing the operation. Write them to disambiguate:
operationId: listUserOrders
summary: List a user's orders
description: >-
Returns orders belonging to the given user, newest first. Supports cursor
pagination and filtering by status. Does not include line items; use
getOrder for those.That one sentence ("does not include line items; use getOrder") prevents a class of wrong agent calls that a name alone cannot.
Enforce it with a linter
Naming conventions are exactly the kind of rule a linter should guarantee rather than a wiki page hoping people remember. A few high-value rules:
- every operation has a unique, camelCase
operationIdmatching an allowed verb prefix; - every operation uses at least one tag that is declared at the root;
- tags and operationIds do not contain version numbers or HTTP method names;
- every operation has a summary;
- no two operations resolve to the same generated method name.
Run the ruleset in CI so a PR that introduces postV2ThingsPost fails before merge, and add a check that flags a brand-new tag, forcing a conscious decision to grow the taxonomy.
What codegen and AI callers do
- openapi-generator, openapi-typescript-style fetchers, and most SDK generators turn
operationIddirectly into the method or function name, often grouping by the first tag. A clean id yieldsclient.users.list()style APIs; a missing id yields path-derived noise. - Mocking and contract tools key recorded examples by operation, so stable ids make mock fixtures and traces readable.
- An AI agent enumerates operations as tools; a precise id plus a disambiguating summary is what lets it pick
listUserOrdersoverlistOrderscorrectly. Ambiguous or missing ids make the agent guess, and the guess lands in production calls.
Checklist
- Set a unique, ASCII, lowerCamelCase, verb-first
operationIdon every operation from the first commit. - Use a fixed verb vocabulary (list, get, create, update/patch, delete, plus domain actions).
- Keep version and HTTP method out of the id; never rename or reuse a published id.
- Declare a small, curated set of domain tags at the root with descriptions.
- Give each operation one primary tag; use tag groups rather than many tags for sections.
- Write a summary and a first description line that disambiguates similar operations for agents.
- Lint uniqueness, casing, allowed verbs, and declared tags in CI.
Get these right and the generated SDK reads like an API a human designed, the docs navigate cleanly at 200 routes, and an AI agent picks the right operation without guessing.
You can apply these conventions, generate a cleanly named TypeScript client, and lint the result all in one local-first workspace, right in your browser. To see how the ids and tags surface in a generated SDK, read generating a TypeScript client from OpenAPI.