From bb4ef96206a3f908e2fbb06bf9658e8c75916078 Mon Sep 17 00:00:00 2001 From: Lagoni Date: Mon, 6 Jul 2026 21:42:35 +0200 Subject: [PATCH] fix: send request body for OpenAPI 3.x operations that declare parameters The request-body extractor treated the presence of an operation `parameters` array as an OpenAPI 2.0 signal and looked for a `in: 'body'` parameter, returning nothing for 3.x operations. But 3.x operations routinely carry `parameters` (path/query/header) alongside a `requestBody`, so any such operation (e.g. a POST with an `X-Correlation-Id` header) had its body silently dropped: the generated context had no `payload` field and the client always sent `body = undefined`. Check `requestBody` first (OpenAPI 3.x) and only fall back to the `in: 'body'` parameter form when no `requestBody` is present (OpenAPI 2.0). Co-Authored-By: Claude Opus 4.8 --- .../inputs/openapi/generators/payloads.ts | 20 ++++--- .../generators/typescript/payload.spec.ts | 8 +++ test/codegen/inputs/openapi/payloads.spec.ts | 60 +++++++++++++++++++ 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 test/codegen/inputs/openapi/payloads.spec.ts diff --git a/src/codegen/inputs/openapi/generators/payloads.ts b/src/codegen/inputs/openapi/generators/payloads.ts index cfcc7fb4..0a3cefa7 100644 --- a/src/codegen/inputs/openapi/generators/payloads.ts +++ b/src/codegen/inputs/openapi/generators/payloads.ts @@ -149,22 +149,26 @@ function extractPayloadsFromOperations( operationObj.operationId ?? `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`; - // Extract request payload schema + // Extract request payload schema. + // Prefer the 3.x `requestBody`: 3.x operations also carry a `parameters` + // array (path/query/header), so presence of `parameters` cannot be used + // to detect 2.0 - doing so would drop the body on any 3.x operation that + // declares parameters. The 2.0 body lives in a `parameters` entry with + // `in: 'body'`, so fall back to that only when there is no `requestBody`. let requestSchema: any = null; - // Check if this is OpenAPI 2.0 vs 3.x based on the structure - if ('parameters' in operationObj && operationObj.parameters) { - // OpenAPI 2.0 style - requestSchema = extractOpenAPI2RequestSchema( - operationObj.parameters as OpenAPIV2.ParameterObject[] - ); - } else if ('requestBody' in operationObj && operationObj.requestBody) { + if ('requestBody' in operationObj && operationObj.requestBody) { // OpenAPI 3.x style requestSchema = extractOpenAPI3RequestSchema( operationObj.requestBody as | OpenAPIV3.RequestBodyObject | OpenAPIV3_1.RequestBodyObject ); + } else if ('parameters' in operationObj && operationObj.parameters) { + // OpenAPI 2.0 style (body carried as a `in: 'body'` parameter) + requestSchema = extractOpenAPI2RequestSchema( + operationObj.parameters as OpenAPIV2.ParameterObject[] + ); } if (requestSchema) { diff --git a/test/codegen/generators/typescript/payload.spec.ts b/test/codegen/generators/typescript/payload.spec.ts index 3201b91e..078c9df4 100644 --- a/test/codegen/generators/typescript/payload.spec.ts +++ b/test/codegen/generators/typescript/payload.spec.ts @@ -193,6 +193,10 @@ describe('payloads', () => { key: "createUsersWithListInput", value: "CreateUsersWithListInputRequest", }, + { + key: "updateUser", + value: "AUser", + }, { key: "addPet_Response", value: "APet", @@ -277,6 +281,10 @@ describe('payloads', () => { key: "createUsersWithListInput", value: "CreateUsersWithListInputRequest", }, + { + key: "updateUser", + value: "AUser", + }, { key: "addPet_Response", value: "APet", diff --git a/test/codegen/inputs/openapi/payloads.spec.ts b/test/codegen/inputs/openapi/payloads.spec.ts new file mode 100644 index 00000000..0696791d --- /dev/null +++ b/test/codegen/inputs/openapi/payloads.spec.ts @@ -0,0 +1,60 @@ +import {OpenAPIV3} from 'openapi-types'; +import {processOpenAPIPayloads} from '../../../../src/codegen/inputs/openapi/generators/payloads'; + +describe('OpenAPI payload extraction', () => { + describe('processOpenAPIPayloads', () => { + it('extracts the request body of a 3.x operation that also declares parameters', () => { + // Regression: a 3.x operation carrying both `parameters` (header/path/query) + // and a `requestBody` must not be mistaken for OpenAPI 2.0 - the body was + // previously dropped whenever any parameter was present. + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: { + '/things': { + post: { + parameters: [ + { + name: 'X-Correlation-Id', + in: 'header', + schema: {type: 'string'} + } + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: {name: {type: 'string'}} + } + } + } + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: {type: 'object', properties: {id: {type: 'string'}}} + } + } + } + } + } + } + } + }; + + const {operationPayloads} = processOpenAPIPayloads(document); + + // The request body surfaces as an operation payload whose schema id ends + // in `Request`; before the fix only the `Response` payload was present. + const schemaIds = Object.values(operationPayloads).map( + (payload) => payload.schemaId + ); + expect(schemaIds.some((id) => id.endsWith('Request'))).toBe(true); + expect(schemaIds.some((id) => id.endsWith('Response'))).toBe(true); + }); + }); +});