From 68ca1c0f345ceb6d206c803a9520d8ed19e6a74e Mon Sep 17 00:00:00 2001 From: Kanna Kim Date: Mon, 13 Jul 2026 22:18:33 -0500 Subject: [PATCH] fix(typescript-fetch): allow null for required nullable parameters Use an undefined-only runtime guard for parameters that are both required and nullable. Preserve the existing null-or-undefined guard for required non-nullable parameters. Add regression coverage for #23493 --- .../resources/typescript-fetch/apis.mustache | 4 +- .../TypeScriptFetchClientCodegenTest.java | 21 +++++++++++ .../3_0/typescript-fetch/issue_23493.yaml | 37 +++++++++++++++++++ .../builds/default-v3.0/apis/FakeApi.ts | 4 +- .../builds/kebab-case/apis/fake-api.ts | 4 +- .../snakecase-discriminator/apis/FakeApi.ts | 4 +- 6 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/typescript-fetch/issue_23493.yaml diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache index 9abbf2590231..161f153b8363 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -118,10 +118,10 @@ export class {{classname}} extends runtime.BaseAPI { async {{nickname}}RequestOpts({{#allParams.0}}requestParameters: {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}Request{{/allParams.0}}): Promise { {{#allParams}} {{#required}} - if (requestParameters['{{paramName}}'] == null) { + if (requestParameters['{{paramName}}'] {{#isBodyParam}}{{#isNullable}}=== undefined{{/isNullable}}{{^isNullable}}== null{{/isNullable}}{{/isBodyParam}}{{^isBodyParam}}== null{{/isBodyParam}}) { throw new runtime.RequiredError( '{{paramName}}', - 'Required parameter "{{paramName}}" was null or undefined when calling {{nickname}}().' + 'Required parameter "{{paramName}}" was {{#isBodyParam}}{{#isNullable}}undefined{{/isNullable}}{{^isNullable}}null or undefined{{/isNullable}}{{/isBodyParam}}{{^isBodyParam}}null or undefined{{/isBodyParam}} when calling {{nickname}}().' ); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java index 5b3b81cdf257..b8e2e2eb7a8f 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java @@ -667,6 +667,27 @@ public void testOptionalNullableFieldDeserializesToNull() throws Exception { TestUtils.assertFileNotContains(modelPath, "json['required_property'] === undefined ? undefined : json['required_property'] === null ? null :"); } + @Test(description = "Required nullable bodies should reject undefined but accept null (fix #23493)") + public void testRequiredNullableBodyRejectsOnlyUndefined() throws Exception { + File output = generate( + Collections.emptyMap(), + "src/test/resources/3_0/typescript-fetch/issue_23493.yaml" + ); + + Path apiPath = Paths.get(output + "/apis/DefaultApi.ts"); + TestUtils.assertFileContains(apiPath, + "if (requestParameters['body'] === undefined)", + "Required parameter \"body\" was undefined when calling nullableBody().", + "body: boolean | null;", + "body: requestParameters['body'] as any"); + TestUtils.assertFileContains(apiPath, + "if (requestParameters['body'] == null)", + "Required parameter \"body\" was null or undefined when calling nonNullableBody()."); + TestUtils.assertFileContains(apiPath, + "if (requestParameters['requiredNullableQuery'] == null)", + "Required parameter \"requiredNullableQuery\" was null or undefined when calling nullableBody()."); + } + @Test(description = "Verify Omit uses the camelCase property name instead of the baseName for readOnly fields") public void testIssue23380_OmitUsesCorrectPropertyName() throws Exception { File output = generate( diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/issue_23493.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/issue_23493.yaml new file mode 100644 index 000000000000..b6f969ac4291 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/issue_23493.yaml @@ -0,0 +1,37 @@ +openapi: 3.0.1 +info: + title: Required nullable request body + version: 1.0.0 +paths: + /nullable: + post: + operationId: nullableBody + parameters: + - name: requiredNullableQuery + in: query + required: true + schema: + type: string + nullable: true + requestBody: + required: true + content: + application/json: + schema: + type: boolean + nullable: true + responses: + '200': + description: OK + /non-nullable: + post: + operationId: nonNullableBody + requestBody: + required: true + content: + application/json: + schema: + type: boolean + responses: + '200': + description: OK diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts index 92463018b31a..dd32c4808dbb 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts @@ -589,10 +589,10 @@ export class FakeApi extends runtime.BaseAPI { * Creates request options for testBodyWithBinary without sending the request */ async testBodyWithBinaryRequestOpts(requestParameters: TestBodyWithBinaryRequest): Promise { - if (requestParameters['body'] == null) { + if (requestParameters['body'] === undefined) { throw new runtime.RequiredError( 'body', - 'Required parameter "body" was null or undefined when calling testBodyWithBinary().' + 'Required parameter "body" was undefined when calling testBodyWithBinary().' ); } diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts index 1eebd5bd8f8a..0f47628e21d0 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts @@ -589,10 +589,10 @@ export class FakeApi extends runtime.BaseAPI { * Creates request options for testBodyWithBinary without sending the request */ async testBodyWithBinaryRequestOpts(requestParameters: TestBodyWithBinaryRequest): Promise { - if (requestParameters['body'] == null) { + if (requestParameters['body'] === undefined) { throw new runtime.RequiredError( 'body', - 'Required parameter "body" was null or undefined when calling testBodyWithBinary().' + 'Required parameter "body" was undefined when calling testBodyWithBinary().' ); } diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts index e7c24607ab6f..a3d509cd3d78 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts @@ -515,10 +515,10 @@ export class FakeApi extends runtime.BaseAPI { * Creates request options for testBodyWithBinary without sending the request */ async testBodyWithBinaryRequestOpts(requestParameters: TestBodyWithBinaryRequest): Promise { - if (requestParameters['body'] == null) { + if (requestParameters['body'] === undefined) { throw new runtime.RequiredError( 'body', - 'Required parameter "body" was null or undefined when calling testBodyWithBinary().' + 'Required parameter "body" was undefined when calling testBodyWithBinary().' ); }