From 2dd410d910f5eedf0c41f3d97aa292afc42feff6 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Tue, 15 Jul 2025 15:27:50 +0100 Subject: [PATCH 01/15] JSDoc stuff --- .../rtk-query-codegen-openapi/src/types.ts | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index 437e058087..bb06733679 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -38,77 +38,76 @@ export interface CommonOptions { */ schemaFile: string; /** - * defaults to "api" + * @default "api" */ apiImport?: string; /** - * defaults to "enhancedApi" + * @default "enhancedApi" */ exportName?: string; /** - * defaults to "ApiArg" + * @default "ApiArg" */ argSuffix?: string; /** - * defaults to "ApiResponse" + * @default "ApiResponse" */ responseSuffix?: string; /** - * defaults to empty + * @default "" */ operationNameSuffix?: string; /** - * defaults to `false` * `true` will generate hooks for queries and mutations, but no lazyQueries + * @default false */ hooks?: boolean | { queries: boolean; lazyQueries: boolean; mutations: boolean }; /** - * defaults to false * `true` will generate a union type for `undefined` properties like: `{ id?: string | undefined }` instead of `{ id?: string }` + * @default false */ unionUndefined?: boolean; /** - * defaults to false * `true` will result in all generated endpoints having `providesTags`/`invalidatesTags` declarations for the `tags` of their respective operation definition + * @default false * @see https://redux-toolkit.js.org/rtk-query/usage/code-generation for more information */ tag?: boolean; /** - * defaults to false * `true` will add `encodeURIComponent` to the generated path parameters + * @default false */ encodePathParams?: boolean; /** - * defaults to false * `true` will add `encodeURIComponent` to the generated query parameters + * @default false */ encodeQueryParams?: boolean; /** - * defaults to false * `true` will "flatten" the arg so that you can do things like `useGetEntityById(1)` instead of `useGetEntityById({ entityId: 1 })` + * @default false */ flattenArg?: boolean; /** - * default to false * If set to `true`, the default response type will be included in the generated code for all endpoints. + * @default false * @see https://swagger.io/docs/specification/describing-responses/#default */ includeDefault?: boolean; /** - * default to false * `true` will not generate separate types for read-only and write-only properties. + * @default false */ mergeReadWriteOnly?: boolean; /** - * * HTTPResolverOptions object that is passed to the SwaggerParser bundle function. */ httpResolverOptions?: SwaggerParser.HTTPResolverOptions; /** - * defaults to undefined * If present the given file will be used as prettier config when formatting the generated code. If undefined the default prettier config * resolution mechanism will be used. + * @default undefined */ prettierConfigFile?: string; } @@ -128,8 +127,8 @@ export interface OutputFileOptions extends Partial { filterEndpoints?: EndpointMatcher; endpointOverrides?: EndpointOverrides[]; /** - * defaults to false * If passed as true it will generate TS enums instead of union of strings + * @default false */ useEnumType?: boolean; } From cd2b656d56348a64859d4b2d1bf5d264aa9ef7b0 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 10:39:11 +0000 Subject: [PATCH 02/15] Add `exactOperationIds` config option --- packages/rtk-query-codegen-openapi/src/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index bb06733679..e10371cceb 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -110,6 +110,11 @@ export interface CommonOptions { * @default undefined */ prettierConfigFile?: string; + + /** + * TODO: Fill out this JSDoc + */ + exactOperationIds?: boolean; } export type TextMatcher = string | RegExp | (string | RegExp)[]; From 91ba30d710d2eb9fcc672434e0844f330196d2fc Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 10:39:32 +0000 Subject: [PATCH 03/15] Use `exactOperationIds` in codegen to not mangle operation IDs when generating code --- .../rtk-query-codegen-openapi/src/generate.ts | 30 ++++++++++++++----- .../src/generators/react-hooks.ts | 21 ++++++++++--- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index d128d100b4..e4d0d1d0dc 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -56,11 +56,17 @@ function patternMatches(pattern?: TextMatcher) { }; } -function operationMatches(pattern?: EndpointMatcher) { +function operationMatches(pattern: EndpointMatcher | undefined, exactOperationIds: boolean) { const checkMatch = typeof pattern === 'function' ? pattern : patternMatches(pattern); return function matcher(operationDefinition: OperationDefinition) { if (!pattern) return true; - const operationName = getOperationName(operationDefinition); + if (exactOperationIds && operationDefinition.operation.operationId === undefined) { + // TODO: More descriptive error message with traceable information + throw new Error('exactOperationIds specified, but operation missing operationId'); + } + const operationName = exactOperationIds + ? operationDefinition.operation.operationId! + : getOperationName(operationDefinition); return checkMatch(operationName, operationDefinition); }; } @@ -89,9 +95,10 @@ function withQueryComment(node: T, def: QueryArgDefinition, h export function getOverrides( operation: OperationDefinition, - endpointOverrides?: EndpointOverrides[] + endpointOverrides?: EndpointOverrides[], + exactOperationIds: boolean = false ): EndpointOverrides | undefined { - return endpointOverrides?.find((override) => operationMatches(override.pattern)(operation)); + return endpointOverrides?.find((override) => operationMatches(override.pattern, exactOperationIds)(operation)); } export async function generateApi( @@ -117,6 +124,7 @@ export async function generateApi( useEnumType = false, mergeReadWriteOnly = false, httpResolverOptions, + exactOperationIds = false, }: GenerationOptions ) { const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions)); @@ -132,7 +140,9 @@ export async function generateApi( apiGen.preprocessComponents(apiGen.spec.components.schemas); } - const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints)); + const operationDefinitions = getOperationDefinitions(v3Doc).filter( + operationMatches(filterEndpoints, exactOperationIds) + ); const resultFile = ts.createSourceFile( 'someFileName.ts', @@ -175,7 +185,8 @@ export async function generateApi( operationDefinitions.map((operationDefinition) => generateEndpoint({ operationDefinition, - overrides: getOverrides(operationDefinition, endpointOverrides), + overrides: getOverrides(operationDefinition, endpointOverrides, exactOperationIds), + exactOperationIds, }) ), true @@ -202,6 +213,7 @@ export async function generateApi( operationDefinitions, endpointOverrides, config: hooks, + exactOperationIds, }), ] : []), @@ -227,9 +239,11 @@ export async function generateApi( function generateEndpoint({ operationDefinition, overrides, + exactOperationIds, }: { operationDefinition: OperationDefinition; overrides?: EndpointOverrides; + exactOperationIds: boolean; }) { const { verb, @@ -238,7 +252,9 @@ export async function generateApi( operation, operation: { responses, requestBody }, } = operationDefinition; - const operationName = getOperationName({ verb, path, operation }); + const operationName = exactOperationIds + ? operation.operationId! // This is a safe operation when using exactOperationIds, as all operation IDs have already been confirmed to exist + : getOperationName({ verb, path, operation }); const tags = tag ? getTags({ verb, pathItem }) : []; const isQuery = testIsQuery(verb, overrides); diff --git a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts index 43232ed07d..70598c218e 100644 --- a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts +++ b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts @@ -11,36 +11,45 @@ type GetReactHookNameParams = { operationDefinition: OperationDefinition; endpointOverrides: EndpointOverrides[] | undefined; config: HooksConfigOptions; + exactOperationIds: boolean; }; type CreateBindingParams = { operationDefinition: OperationDefinition; overrides?: EndpointOverrides; isLazy?: boolean; + exactOperationIds: boolean; }; const createBinding = ({ operationDefinition: { verb, path, operation }, overrides, isLazy = false, + exactOperationIds, }: CreateBindingParams) => factory.createBindingElement( undefined, undefined, factory.createIdentifier( - `use${isLazy ? 'Lazy' : ''}${capitalize(getOperationName(verb, path, operation.operationId))}${ + `use${isLazy ? 'Lazy' : ''}${capitalize(exactOperationIds ? operation.operationId! : getOperationName(verb, path, operation.operationId))}${ isQuery(verb, overrides) ? 'Query' : 'Mutation' }` ), undefined ); -const getReactHookName = ({ operationDefinition, endpointOverrides, config }: GetReactHookNameParams) => { - const overrides = getOverrides(operationDefinition, endpointOverrides); +const getReactHookName = ({ + operationDefinition, + endpointOverrides, + config, + exactOperationIds, +}: GetReactHookNameParams) => { + const overrides = getOverrides(operationDefinition, endpointOverrides, exactOperationIds); const baseParams = { operationDefinition, overrides, + exactOperationIds, }; const _isQuery = isQuery(operationDefinition.verb, overrides); @@ -66,12 +75,14 @@ type GenerateReactHooksParams = { operationDefinitions: OperationDefinition[]; endpointOverrides: EndpointOverrides[] | undefined; config: HooksConfigOptions; + exactOperationIds: boolean; }; export const generateReactHooks = ({ exportName, operationDefinitions, endpointOverrides, config, + exactOperationIds, }: GenerateReactHooksParams) => factory.createVariableStatement( [factory.createModifier(ts.SyntaxKind.ExportKeyword)], @@ -80,7 +91,9 @@ export const generateReactHooks = ({ factory.createVariableDeclaration( factory.createObjectBindingPattern( operationDefinitions - .map((operationDefinition) => getReactHookName({ operationDefinition, endpointOverrides, config })) + .map((operationDefinition) => + getReactHookName({ operationDefinition, endpointOverrides, config, exactOperationIds }) + ) .flat() ), undefined, From 808432932f80a2babe092980cf45278da72cbdd0 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 10:46:38 +0000 Subject: [PATCH 04/15] Fix --- .../src/generators/react-hooks.ts | 10 ++++++++-- packages/rtk-query-codegen-openapi/src/types.ts | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts index 161bb7ce13..613ba2a664 100644 --- a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts +++ b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts @@ -46,7 +46,7 @@ const getReactHookName = ({ endpointOverrides, config, exactOperationIds, - operationNameSuffix + operationNameSuffix, }: GetReactHookNameParams) => { const overrides = getOverrides(operationDefinition, endpointOverrides, exactOperationIds); @@ -99,7 +99,13 @@ export const generateReactHooks = ({ factory.createObjectBindingPattern( operationDefinitions .map((operationDefinition) => - getReactHookName({ operationDefinition, endpointOverrides, config, exactOperationIds, operationNameSuffix }) + getReactHookName({ + operationDefinition, + endpointOverrides, + config, + exactOperationIds, + operationNameSuffix, + }) ) .flat() ), diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index e702f5537a..a89cf49e8e 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -112,9 +112,6 @@ export interface CommonOptions { prettierConfigFile?: string; /** - * TODO: Fill out this JSDoc - */ - exactOperationIds?: boolean; * Determines the fallback type for empty schemas. * * If set to **`true`**, **`unknown`** will be used @@ -124,16 +121,23 @@ export interface CommonOptions { * @since 2.1.0 */ useUnknown?: boolean; + /** * @default false * Will generate imports with file extension matching the expected compiled output of the api file */ esmExtensions?: boolean; + /** * @default false * Will generate regex constants for pattern keywords in the schema */ outputRegexConstants?: boolean; + + /** + * TODO: Fill out this JSDoc + */ + exactOperationIds?: boolean; } export type TextMatcher = string | RegExp | (string | RegExp)[]; From a2ed8a34c4f10652365379d719de55560f0d4ed4 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 10:49:13 +0000 Subject: [PATCH 05/15] Ordering --- packages/rtk-query-codegen-openapi/src/generate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index d20068dc9d..70ddd2b471 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -174,10 +174,10 @@ export async function generateApi( useEnumType = false, mergeReadWriteOnly = false, httpResolverOptions, - exactOperationIds = false, useUnknown = false, esmExtensions = false, outputRegexConstants = false, + exactOperationIds = false, }: GenerationOptions ) { const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions)); @@ -288,8 +288,8 @@ export async function generateApi( operationDefinitions, endpointOverrides, config: hooks, - exactOperationIds, operationNameSuffix, + exactOperationIds, }), ] : []), From 46c4b5c73ce1f52810da577f5164d09d3975767d Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 10:51:15 +0000 Subject: [PATCH 06/15] More ordering --- .../src/generators/react-hooks.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts index 613ba2a664..57d52c3564 100644 --- a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts +++ b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts @@ -11,24 +11,24 @@ type GetReactHookNameParams = { operationDefinition: OperationDefinition; endpointOverrides: EndpointOverrides[] | undefined; config: HooksConfigOptions; - exactOperationIds: boolean; operationNameSuffix?: string; + exactOperationIds: boolean; }; type CreateBindingParams = { operationDefinition: OperationDefinition; overrides?: EndpointOverrides; isLazy?: boolean; - exactOperationIds: boolean; operationNameSuffix?: string; + exactOperationIds: boolean; }; const createBinding = ({ operationDefinition: { verb, path, operation }, overrides, isLazy = false, - exactOperationIds, operationNameSuffix, + exactOperationIds, }: CreateBindingParams) => factory.createBindingElement( undefined, @@ -45,16 +45,16 @@ const getReactHookName = ({ operationDefinition, endpointOverrides, config, - exactOperationIds, operationNameSuffix, + exactOperationIds, }: GetReactHookNameParams) => { const overrides = getOverrides(operationDefinition, endpointOverrides, exactOperationIds); const baseParams = { operationDefinition, overrides, - exactOperationIds, operationNameSuffix, + exactOperationIds, }; const _isQuery = isQuery(operationDefinition.verb, overrides); @@ -80,16 +80,16 @@ type GenerateReactHooksParams = { operationDefinitions: OperationDefinition[]; endpointOverrides: EndpointOverrides[] | undefined; config: HooksConfigOptions; - exactOperationIds: boolean; operationNameSuffix?: string; + exactOperationIds: boolean; }; export const generateReactHooks = ({ exportName, operationDefinitions, endpointOverrides, config, - exactOperationIds, operationNameSuffix, + exactOperationIds, }: GenerateReactHooksParams) => factory.createVariableStatement( [factory.createModifier(ts.SyntaxKind.ExportKeyword)], @@ -103,8 +103,8 @@ export const generateReactHooks = ({ operationDefinition, endpointOverrides, config, - exactOperationIds, operationNameSuffix, + exactOperationIds, }) ) .flat() From 873ebdc834ffd921cce90da62f896fc436dcf183 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 11:06:26 +0000 Subject: [PATCH 07/15] Add test stubs --- .../test/generateEndpoints.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index 20c895219d..8afe0e153f 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -933,3 +933,19 @@ describe('esmExtensions option', () => { expect(content).toContain("import { api } from '../../fixtures/emptyApi'"); }); }); + +describe('exactOperationIds option', () => { + test('regular camelCase operation IDs are kept the same', async () => {}); + + test('operations missing operation ID should not be allowed', async () => {}); + + test('duplicate operation IDs should not be allowed', async () => {}); + + describe('operation IDs with subsequent uppercase characters', () => { + test('should not be changed in endpoint definition key', async () => {}); + + test('should not be changed in exported hook name', async () => {}); + + test('should not be changed in generated type names', async () => {}); + }); +}); From 431ad226c4a1f28e56d6d351b008c5dc0ee5712f Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 11:20:27 +0000 Subject: [PATCH 08/15] Add duplicate operation ID check --- packages/rtk-query-codegen-openapi/src/generate.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 70ddd2b471..1a6441903a 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -198,6 +198,16 @@ export async function generateApi( operationMatches(filterEndpoints, exactOperationIds) ); + if (exactOperationIds) { + const duplicateOperationIds = operationDefinitions + .map((o) => o.operation.operationId!) + .filter((operationId, index, allOperationIds) => allOperationIds.findIndex((id) => id === operationId) !== index); + if (duplicateOperationIds.length > 0) { + // TODO: More descriptive error message with traceable information + throw new Error('Duplicate operation IDs not allowed when using exactOperationIds'); + } + } + const resultFile = ts.createSourceFile( 'someFileName.ts', '', From 7fb57124709a82fee301e8661e80ff704400a477 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 11:26:15 +0000 Subject: [PATCH 09/15] Remove temporary workaround as no longer needed --- packages/rtk-query-codegen-openapi/src/generate.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 1a6441903a..1744baeb4c 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -189,11 +189,6 @@ export async function generateApi( useUnknown, }); - // temporary workaround for https://github.com/oazapfts/oazapfts/issues/491 - if (apiGen.spec.components?.schemas) { - apiGen.preprocessComponents(apiGen.spec.components.schemas); - } - const operationDefinitions = getOperationDefinitions(v3Doc).filter( operationMatches(filterEndpoints, exactOperationIds) ); From 34e61371678e5051e34931dcea19bd01a95b4591 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 11:51:09 +0000 Subject: [PATCH 10/15] Implement tests --- .../test/fixtures/exactOperationId.yaml | 375 ++++++++++++++++++ .../exactOperationIdMissingOperationId.yaml | 374 +++++++++++++++++ .../test/generateEndpoints.test.ts | 48 ++- 3 files changed, 791 insertions(+), 6 deletions(-) create mode 100644 packages/rtk-query-codegen-openapi/test/fixtures/exactOperationId.yaml create mode 100644 packages/rtk-query-codegen-openapi/test/fixtures/exactOperationIdMissingOperationId.yaml diff --git a/packages/rtk-query-codegen-openapi/test/fixtures/exactOperationId.yaml b/packages/rtk-query-codegen-openapi/test/fixtures/exactOperationId.yaml new file mode 100644 index 0000000000..38ae045d1d --- /dev/null +++ b/packages/rtk-query-codegen-openapi/test/fixtures/exactOperationId.yaml @@ -0,0 +1,375 @@ +openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + version: 1.0.5 +servers: + - url: /api/v3 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io + - name: store + description: Operations about user + - name: user + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePETScan + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPETScansByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /user/{username}: + get: + tags: + - user + summary: Get user by user name + description: 'conflicting duplicate operation ID' + operationId: findPETScansByStatus + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Customer: + type: object + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + xml: + name: addresses + wrapped: true + items: + $ref: '#/components/schemas/Address' + xml: + name: customer + Address: + type: object + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: '94301' + xml: + name: address + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + example: john@email.com + password: + type: string + pattern: '' + example: '12345' + phone: + type: string + pattern: '^\+?[1-9]\d{1,14}$' + example: '12345' + website: + type: string + pattern: '^https?://[^\s]+$' + example: 'https://example.com' + userStatus: + type: integer + pattern: '^[1-9]\d{0,2}$' + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^\S+$' + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/packages/rtk-query-codegen-openapi/test/fixtures/exactOperationIdMissingOperationId.yaml b/packages/rtk-query-codegen-openapi/test/fixtures/exactOperationIdMissingOperationId.yaml new file mode 100644 index 0000000000..3d3be4bce7 --- /dev/null +++ b/packages/rtk-query-codegen-openapi/test/fixtures/exactOperationIdMissingOperationId.yaml @@ -0,0 +1,374 @@ +openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + version: 1.0.5 +servers: + - url: /api/v3 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io + - name: store + description: Operations about user + - name: user + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Missing operation ID - this should cause test to error when using exactOperationIds + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPETScansByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /user/{username}: + get: + tags: + - user + summary: Get user by user name + description: 'conflicting duplicate operation ID' + operationId: findPETScansByStatus + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Customer: + type: object + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + xml: + name: addresses + wrapped: true + items: + $ref: '#/components/schemas/Address' + xml: + name: customer + Address: + type: object + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: '94301' + xml: + name: address + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + example: john@email.com + password: + type: string + pattern: '' + example: '12345' + phone: + type: string + pattern: '^\+?[1-9]\d{1,14}$' + example: '12345' + website: + type: string + pattern: '^https?://[^\s]+$' + example: 'https://example.com' + userStatus: + type: integer + pattern: '^[1-9]\d{0,2}$' + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^\S+$' + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index 8afe0e153f..05f2a4b01f 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -935,17 +935,53 @@ describe('esmExtensions option', () => { }); describe('exactOperationIds option', () => { - test('regular camelCase operation IDs are kept the same', async () => {}); + const generateCode = (filterEndpoints: Array, schemaFile: string = 'fixtures/exactOperationId.yaml') => + generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, schemaFile), + filterEndpoints, + hooks: true, + exactOperationIds: true, + }); - test('operations missing operation ID should not be allowed', async () => {}); + test('regular camelCase operation IDs are kept the same', async () => { + const content = await generateCode(['addPet']); + expect(content).toContain('addPet: build.mutation<'); + expect(content).toContain('type AddPetApiResponse'); + expect(content).toContain('type AddPetApiArg'); + expect(content).toContain('useAddPetMutation'); + }); - test('duplicate operation IDs should not be allowed', async () => {}); + test('operations missing operation ID should not be allowed', async () => { + expect( + async () => await generateCode(['addPet'], 'fixtures/exactOperationIdMissingOperationId.yaml') + ).rejects.toThrow(new Error('exactOperationIds specified, but found operation missing operationId')); + }); + + test('duplicate operation IDs should not be allowed', async () => { + expect(async () => await generateCode(['addPet', 'findPETScansByStatus'])).rejects.toThrow( + new Error('Duplicate operation IDs not allowed when using exactOperationIds') + ); + }); describe('operation IDs with subsequent uppercase characters', () => { - test('should not be changed in endpoint definition key', async () => {}); + let content: string; + + beforeAll(async () => { + content = (await generateCode(['updatePETScan', 'addPet'])) as string; + }); - test('should not be changed in exported hook name', async () => {}); + test('should not be changed in endpoint definition key', async () => { + expect(content).toContain('updatePETScan: build.mutation'); + }); - test('should not be changed in generated type names', async () => {}); + test('should not be changed in exported hook name', async () => { + expect(content).toContain('useUpdatePETScanMutation'); + }); + + test('should not be changed in generated type names', async () => { + expect(content).toContain('UpdatePETScanApiResponse'); + expect(content).toContain('UpdatePETScanApiArg'); + }); }); }); From b2ab805adb50b4a69f0034e8b59f3b403c2dfd37 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 11:51:19 +0000 Subject: [PATCH 11/15] Better error message --- packages/rtk-query-codegen-openapi/src/generate.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 1744baeb4c..f81b212822 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -62,7 +62,7 @@ function operationMatches(pattern: EndpointMatcher | undefined, exactOperationId if (!pattern) return true; if (exactOperationIds && operationDefinition.operation.operationId === undefined) { // TODO: More descriptive error message with traceable information - throw new Error('exactOperationIds specified, but operation missing operationId'); + throw new Error('exactOperationIds specified, but found operation missing operationId'); } const operationName = exactOperationIds ? operationDefinition.operation.operationId! @@ -189,6 +189,11 @@ export async function generateApi( useUnknown, }); + // temporary workaround for https://github.com/oazapfts/oazapfts/issues/491 + if (apiGen.spec.components?.schemas) { + apiGen.preprocessComponents(apiGen.spec.components.schemas); + } + const operationDefinitions = getOperationDefinitions(v3Doc).filter( operationMatches(filterEndpoints, exactOperationIds) ); From 133d11bfdc62e05b49428dc26b4e6d824c9c5121 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 13:33:04 +0000 Subject: [PATCH 12/15] Extract --- packages/rtk-query-codegen-openapi/src/generate.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index f81b212822..aa9b6df64f 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -199,9 +199,10 @@ export async function generateApi( ); if (exactOperationIds) { - const duplicateOperationIds = operationDefinitions - .map((o) => o.operation.operationId!) - .filter((operationId, index, allOperationIds) => allOperationIds.findIndex((id) => id === operationId) !== index); + const allOperationIds = operationDefinitions.map((o) => o.operation.operationId!); + const duplicateOperationIds = allOperationIds.filter( + (operationId, index) => allOperationIds.findIndex((id) => id === operationId) !== index + ); if (duplicateOperationIds.length > 0) { // TODO: More descriptive error message with traceable information throw new Error('Duplicate operation IDs not allowed when using exactOperationIds'); From 90f9c35df483ef1783304826d9c36ca69fcec007 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 13:33:09 +0000 Subject: [PATCH 13/15] Add note to docs --- docs/rtk-query/usage/code-generation.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/rtk-query/usage/code-generation.mdx b/docs/rtk-query/usage/code-generation.mdx index f307339267..6243fc9525 100644 --- a/docs/rtk-query/usage/code-generation.mdx +++ b/docs/rtk-query/usage/code-generation.mdx @@ -158,6 +158,13 @@ const filteredConfig: ConfigFile = { } ``` +:::note +You can avoid transforming endpoint names to camel case by passing `exactOperationIds: true` to `generateEndpoints`. However, you will have to ensure the following: +- All endpoints in use have an operation ID +- You have no duplicate operation IDs +- None of your operation IDs include whitespace +::: + #### Endpoint overrides If an endpoint is generated as a mutation instead of a query or the other way round, you can override that: From 1433fea1e90e74224919bead1a2f896c13165c44 Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 13:34:35 +0000 Subject: [PATCH 14/15] Add JSDoc description --- packages/rtk-query-codegen-openapi/src/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index a89cf49e8e..cf6db1a2b9 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -135,7 +135,9 @@ export interface CommonOptions { outputRegexConstants?: boolean; /** - * TODO: Fill out this JSDoc + * @default false + * Will use the original operation IDs from the schema instead of converting them to camel case. + * May cause issues if you have duplicate/missing operation IDs */ exactOperationIds?: boolean; } From 561ce404ecd231e0a5b56db154f23fa616c4ed9b Mon Sep 17 00:00:00 2001 From: Issy Szemeti Date: Fri, 2 Jan 2026 14:11:24 +0000 Subject: [PATCH 15/15] Add function overloads --- packages/rtk-query-codegen-openapi/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rtk-query-codegen-openapi/src/index.ts b/packages/rtk-query-codegen-openapi/src/index.ts index 41a251d63d..d890957b1a 100644 --- a/packages/rtk-query-codegen-openapi/src/index.ts +++ b/packages/rtk-query-codegen-openapi/src/index.ts @@ -8,6 +8,8 @@ export type { ConfigFile } from './types'; const require = createRequire(__filename); +export async function generateEndpoints(options: GenerationOptions & { outputFile: string }): Promise; +export async function generateEndpoints(options: GenerationOptions & { outputFile?: never }): Promise; export async function generateEndpoints(options: GenerationOptions): Promise { const schemaLocation = options.schemaFile;