From 9234ed2f545dbc7cb9e94b1e3b28ade3549f8e50 Mon Sep 17 00:00:00 2001 From: Mahin Anowar <86069420+MahinAnowar@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:43:44 +0600 Subject: [PATCH] fix(codegen): respect optional request bodies The request-body arg was hardcoded to `required: true`, so every operation with a request body generated a non-optional arg (`body: T`) even when the OpenAPI spec did not mark the body as required. Per the spec, `requestBody.required` defaults to `false`, so these args should be optional (`body?: T`). Use `body.required ?? false` instead, mirroring how query/path params already use `param.required`. Snapshots update accordingly: bodies not marked `required: true` now become optional, while bodies with `required: true` (e.g. petstore's `pet`) stay required. Fixes #4824 --- .../rtk-query-codegen-openapi/src/generate.ts | 3 +- .../test/__snapshots__/cli.test.ts.snap | 20 +-- .../generateEndpoints.test.ts.snap | 116 +++++++++--------- .../test/fixtures/issue-4824.json | 58 +++++++++ .../test/generateEndpoints.test.ts | 14 +++ 5 files changed, 142 insertions(+), 69 deletions(-) create mode 100644 packages/rtk-query-codegen-openapi/test/fixtures/issue-4824.json diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 220b163d7c..16a78769b3 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -444,7 +444,8 @@ export async function generateApi( name, originalName: schemaName, type: getTypeFromSchema(withMode(ctx, 'writeOnly'), schema), - required: true, + // A request body is optional unless the spec explicitly marks it required (OpenAPI defaults `required` to `false`). + required: body.required ?? false, body, }; } diff --git a/packages/rtk-query-codegen-openapi/test/__snapshots__/cli.test.ts.snap b/packages/rtk-query-codegen-openapi/test/__snapshots__/cli.test.ts.snap index afd08b3847..8da01c5688 100644 --- a/packages/rtk-query-codegen-openapi/test/__snapshots__/cli.test.ts.snap +++ b/packages/rtk-query-codegen-openapi/test/__snapshots__/cli.test.ts.snap @@ -147,7 +147,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -155,7 +155,7 @@ export type GetInventoryApiResponse = /** status 200 successful operation */ { export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; export type GetOrderByIdApiArg = { @@ -170,11 +170,11 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; export type LoginUserApiArg = { @@ -195,7 +195,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -395,7 +395,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -403,7 +403,7 @@ export type GetInventoryApiResponse = /** status 200 successful operation */ { export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; export type GetOrderByIdApiArg = { @@ -418,11 +418,11 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; export type LoginUserApiArg = { @@ -443,7 +443,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { diff --git a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap index 6e7a7c29d7..bded3e7b4d 100644 --- a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap +++ b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap @@ -147,7 +147,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -155,7 +155,7 @@ export type GetInventoryApiResponse = /** status 200 successful operation */ { export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; export type GetOrderByIdApiArg = { @@ -170,11 +170,11 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; export type LoginUserApiArg = { @@ -195,7 +195,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -395,7 +395,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -403,7 +403,7 @@ export type GetInventoryApiResponse = /** status 200 successful operation */ { export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; export type GetOrderByIdApiArg = { @@ -418,11 +418,11 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; export type LoginUserApiArg = { @@ -443,7 +443,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -694,7 +694,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -703,7 +703,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -719,12 +719,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -747,7 +747,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -879,7 +879,7 @@ export { injectedRtkApi as enhancedApi }; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -1105,7 +1105,7 @@ export type UploadFileApiResponse = export type UploadFileApiArg = { /** ID of pet to update */ petId: number; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -1114,7 +1114,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -1130,12 +1130,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -1153,7 +1153,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -1384,7 +1384,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -1393,7 +1393,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -1409,12 +1409,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -1435,7 +1435,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -1682,7 +1682,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -1691,7 +1691,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -1707,12 +1707,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -1735,7 +1735,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -1966,7 +1966,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -1975,7 +1975,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -1991,12 +1991,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -2017,7 +2017,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -2242,7 +2242,7 @@ export type UploadFileApiResponse = export type UploadFileApiArg = { /** ID of pet to update */ petId: number; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -2251,7 +2251,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -2267,12 +2267,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -2290,7 +2290,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -2367,11 +2367,11 @@ const injectedRtkApi = api.injectEndpoints({ export { injectedRtkApi as enhancedApi }; export type PostV1ExportApiResponse = unknown; export type PostV1ExportApiArg = { - exportedEntityIds: IdList; + exportedEntityIds?: IdList; }; export type PostV1ImportApiResponse = unknown; export type PostV1ImportApiArg = { - rawData: string; + rawData?: string; }; export type IdList = number[]; " @@ -2795,7 +2795,7 @@ export type UploadFileV2ApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryV2ApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -2804,7 +2804,7 @@ export type GetInventoryV2ApiArg = void; export type PlaceOrderV2ApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderV2ApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdV2ApiResponse = /** status 200 successful operation */ Order; @@ -2820,12 +2820,12 @@ export type DeleteOrderV2ApiArg = { export type CreateUserV2ApiResponse = unknown; export type CreateUserV2ApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputV2ApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputV2ApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserV2ApiResponse = /** status 200 successful operation */ string; @@ -2848,7 +2848,7 @@ export type UpdateUserV2ApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserV2ApiResponse = unknown; export type DeleteUserV2ApiArg = { @@ -3474,7 +3474,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -3483,7 +3483,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -3499,12 +3499,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -3527,7 +3527,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { @@ -3876,7 +3876,7 @@ export type UploadFileApiArg = { petId: number; /** Additional Metadata */ additionalMetadata?: string; - body: Blob; + body?: Blob; }; export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; @@ -3885,7 +3885,7 @@ export type GetInventoryApiArg = void; export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { - order: Order; + order?: Order; }; export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; @@ -3901,12 +3901,12 @@ export type DeleteOrderApiArg = { export type CreateUserApiResponse = unknown; export type CreateUserApiArg = { /** Created user object */ - user: User; + user?: User; }; export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { - body: User[]; + body?: User[]; }; export type LoginUserApiResponse = /** status 200 successful operation */ string; @@ -3929,7 +3929,7 @@ export type UpdateUserApiArg = { /** name that need to be deleted */ username: string; /** Update an existent user in the store */ - user: User; + user?: User; }; export type DeleteUserApiResponse = unknown; export type DeleteUserApiArg = { diff --git a/packages/rtk-query-codegen-openapi/test/fixtures/issue-4824.json b/packages/rtk-query-codegen-openapi/test/fixtures/issue-4824.json new file mode 100644 index 0000000000..e35a74088d --- /dev/null +++ b/packages/rtk-query-codegen-openapi/test/fixtures/issue-4824.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Issue 4824 - optional request body", + "version": "1.0.0" + }, + "paths": { + "/foo": { + "delete": { + "operationId": "deleteFoo", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/bar": { + "post": { + "operationId": "createBar", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index d08c5e48bf..fd40eb1786 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -743,6 +743,20 @@ describe('tests from issues', () => { expect(result).toMatchSnapshot(); }); + + it('issue #4824: an optional request body should generate an optional arg', async () => { + const result = await generateEndpoints({ + apiFile: './test/fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/issue-4824.json'), + }); + + // `deleteFoo` has a request body without `required: true`, so the generated arg must be optional. + expect(result).toMatch(/export type DeleteFooApiArg = \{\s*body\?:/); + + // `createBar` explicitly sets `required: true`, so its body arg must stay required. + expect(result).toMatch(/export type CreateBarApiArg = \{\s*body:/); + expect(result).not.toMatch(/export type CreateBarApiArg = \{\s*body\?:/); + }); }); describe('openapi spec', () => {