From 0dec11830a3555a2963ccc648b1519713a6e791a Mon Sep 17 00:00:00 2001 From: Wiebren Braakman Date: Fri, 28 Aug 2026 22:24:39 +0200 Subject: [PATCH 1/5] fix: [typescript-fetch] explode object query parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A query parameter whose schema is an object and whose style/explode are left at their defaults — style: form, explode: true — must go on the wire as one parameter per entry, keyed by the property name alone. The typescript-fetch client bracketed it as filter[category]=books instead. typescript-fetch/apis.mustache gated the explode branch on isContainer, which DefaultCodegen.fromParameter only sets via updateParameterForMap, which in turn needs ModelUtils.isMapSchema (so, additionalProperties). A bare type: object instead goes through setTypeProperties, which sets isMap and isFreeFormObject and leaves isContainer false. The parameter was therefore assigned whole and the runtime's querystringSingleKey bracketed it. The branch is now keyed on isMap; inside {{^isArray}}, isContainer implies isMap, so this is the same condition plus free-form objects. TypeScriptFetchClientCodegen's ExtendedCodegenParameter copy constructor copied isExplode and style but not isDeepObject, isFormStyle, isMatrix, isAllowEmptyValue, isSpaceDelimited or isPipeDelimited, so all six were false in every typescript-fetch template regardless of the document. This is independent of the template fix, and it is why fixing the template alone would have broken deepObject parameters — they are explode: true too. The loop body gains an `as any` cast on the indexing. It is not needed for a declared map, but a free-form object is typed `object` and object[key] is error TS7053 under strict, which is how most consumers compile. --- .../TypeScriptFetchClientCodegen.java | 6 +++ .../resources/typescript-fetch/apis.mustache | 20 ++++++-- .../TypeScriptFetchClientCodegenTest.java | 27 ++++++++++ .../3_0/exploded-object-query-param.yaml | 51 +++++++++++++++++++ .../builds/default-v3.0/apis/FakeApi.ts | 2 +- .../builds/kebab-case/apis/fake-api.ts | 2 +- .../snakecase-discriminator/apis/FakeApi.ts | 2 +- 7 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index c326ae929ab4..2a424b80a217 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -1559,6 +1559,12 @@ public ExtendedCodegenParameter(CodegenParameter cp) { this.isPrimitiveType = cp.isPrimitiveType; this.isModel = cp.isModel; this.isExplode = cp.isExplode; + this.isDeepObject = cp.isDeepObject; + this.isMatrix = cp.isMatrix; + this.isAllowEmptyValue = cp.isAllowEmptyValue; + this.isFormStyle = cp.isFormStyle; + this.isSpaceDelimited = cp.isSpaceDelimited; + this.isPipeDelimited = cp.isPipeDelimited; this.baseName = cp.baseName; this.paramName = cp.paramName; this.dataType = cp.dataType; 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 339410685eaf..59d0dd05cff7 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -202,14 +202,24 @@ export class {{classname}} extends runtime.BaseAPI { {{^isArray}} if (requestParameters['{{paramName}}'] != null) { {{#isExplode}} - {{#isContainer}} + {{! An exploded object becomes one parameter per entry, keyed by the property name + alone. isMap covers both a declared map and a free-form object; isContainer is + not set for a free-form object, so it cannot be used here. deepObject is + explode: true as well, but nests the property under the parameter name, which + the runtime does when the object is assigned as a whole. }} + {{#isDeepObject}} +{{>apisAssignQueryParam}} + {{/isDeepObject}} + {{^isDeepObject}} + {{#isMap}} for (let key of Object.keys(requestParameters['{{paramName}}'])) { - queryParameters[key] = requestParameters['{{paramName}}'][key]; + queryParameters[key] = (requestParameters['{{paramName}}'] as any)[key]; } - {{/isContainer}} - {{^isContainer}} + {{/isMap}} + {{^isMap}} {{>apisAssignQueryParam}} - {{/isContainer}} + {{/isMap}} + {{/isDeepObject}} {{/isExplode}} {{^isExplode}} {{>apisAssignQueryParam}} 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 4c9c71f1f2f2..3c599ace8713 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 @@ -1177,6 +1177,33 @@ public void testDateFormatIsSerializedAsACalendarDate() throws IOException { TestUtils.assertFileContains(api, "formParams.append('createdAt', runtime.serializeDateTime(requestParameters['createdAt'] as any))"); } + @Test(description = "Verify an object query parameter is exploded, whether or not it declares its properties") + public void testExplodedObjectQueryParameter() throws IOException { + File output = generate(new HashMap<>(), "src/test/resources/3_0/exploded-object-query-param.yaml"); + Path api = Paths.get(output + "/apis/DefaultApi.ts"); + + // form style with explode - the default - puts every entry on the wire under its own + // property name. A free-form object is isMap but not isContainer, so it used to fall + // through to a whole-object assignment and end up bracketed by the runtime. + TestUtils.assertFileContains(api, + "for (let key of Object.keys(requestParameters['filter'])) {", + "queryParameters[key] = (requestParameters['filter'] as any)[key];"); + TestUtils.assertFileNotContains(api, "queryParameters['filter'] = requestParameters['filter'];"); + + // a declared map behaves the same way + TestUtils.assertFileContains(api, + "for (let key of Object.keys(requestParameters['typedFilter'])) {", + "queryParameters[key] = (requestParameters['typedFilter'] as any)[key];"); + + // deepObject nests under the parameter name, which the runtime does for a whole object + TestUtils.assertFileContains(api, "queryParameters['deepFilter'] = requestParameters['deepFilter'];"); + TestUtils.assertFileNotContains(api, "Object.keys(requestParameters['deepFilter'])"); + + // without explode the object stays a single parameter + TestUtils.assertFileContains(api, "queryParameters['flatFilter'] = requestParameters['flatFilter'];"); + TestUtils.assertFileNotContains(api, "Object.keys(requestParameters['flatFilter'])"); + } + private static final String DATE_HANDLING_SPEC = "src/test/resources/3_0/typescript-fetch/date-handling.yaml"; private static File generate( diff --git a/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml b/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml new file mode 100644 index 000000000000..fd6fce7d5807 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.3 +info: + title: Exploded object query parameters + description: > + Object typed query parameters, covering the four combinations of style and explode that + decide how an object is put on the wire. The free-form variants matter because a + free-form object is flagged isMap but not isContainer. + version: 1.0.0 +servers: + - url: localhost:8080 +paths: + /items: + get: + operationId: listItems + parameters: + # style and explode both left out, so the form/true defaults apply: every entry + # becomes its own parameter, keyed by the property name alone. + - in: query + name: filter + schema: + type: object + # the same, but declared as a map rather than as a free-form object + - in: query + name: typedFilter + schema: + type: object + additionalProperties: + type: string + # deepObject nests each entry under the parameter name: deepFilter[key]=value + - in: query + name: deepFilter + style: deepObject + explode: true + schema: + type: object + # form without explode keeps a single parameter carrying the whole object + - in: query + name: flatFilter + style: form + explode: false + schema: + type: object + responses: + '200': + description: a list of items + content: + application/json: + schema: + type: array + items: + type: string 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 f64fcdc89bf5..07c04b2fd3da 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 @@ -1560,7 +1560,7 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = requestParameters['language'][key]; + queryParameters[key] = (requestParameters['language'] as any)[key]; } } 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 290f6b63a71d..590f15518042 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 @@ -1560,7 +1560,7 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = requestParameters['language'][key]; + queryParameters[key] = (requestParameters['language'] as any)[key]; } } 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 992042979312..aea78c920a9d 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 @@ -1378,7 +1378,7 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = requestParameters['language'][key]; + queryParameters[key] = (requestParameters['language'] as any)[key]; } } From 500c829c819af0001c684cc1b5e4c0bf8cf8b428 Mon Sep 17 00:00:00 2001 From: Wiebren Braakman Date: Wed, 23 Sep 2026 09:11:02 +0200 Subject: [PATCH 2/5] fix: [typescript-fetch] leave out a null entry of an exploded object The runtime turned a null or undefined entry into k=null or k=undefined; the loop now skips it, as the other ports do. Only isDeepObject is read by the templates, so the copy constructor keeps that flag alone. The mustache comment shrinks to one line, and the shared fixture's description names the three combinations it covers. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../languages/TypeScriptFetchClientCodegen.java | 5 ----- .../src/main/resources/typescript-fetch/apis.mustache | 11 +++++------ .../fetch/TypeScriptFetchClientCodegenTest.java | 9 ++++++--- .../resources/3_0/exploded-object-query-param.yaml | 4 +--- .../builds/default-v3.0/apis/FakeApi.ts | 5 ++++- .../builds/kebab-case/apis/fake-api.ts | 5 ++++- .../builds/snakecase-discriminator/apis/FakeApi.ts | 5 ++++- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index 2a424b80a217..b33bd47ed861 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -1560,11 +1560,6 @@ public ExtendedCodegenParameter(CodegenParameter cp) { this.isModel = cp.isModel; this.isExplode = cp.isExplode; this.isDeepObject = cp.isDeepObject; - this.isMatrix = cp.isMatrix; - this.isAllowEmptyValue = cp.isAllowEmptyValue; - this.isFormStyle = cp.isFormStyle; - this.isSpaceDelimited = cp.isSpaceDelimited; - this.isPipeDelimited = cp.isPipeDelimited; this.baseName = cp.baseName; this.paramName = cp.paramName; this.dataType = cp.dataType; 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 59d0dd05cff7..776ed362ee5a 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -202,18 +202,17 @@ export class {{classname}} extends runtime.BaseAPI { {{^isArray}} if (requestParameters['{{paramName}}'] != null) { {{#isExplode}} - {{! An exploded object becomes one parameter per entry, keyed by the property name - alone. isMap covers both a declared map and a free-form object; isContainer is - not set for a free-form object, so it cannot be used here. deepObject is - explode: true as well, but nests the property under the parameter name, which - the runtime does when the object is assigned as a whole. }} + {{! form + explode: one parameter per entry. isMap (not isContainer) also covers free-form objects; deepObject is assigned whole so the runtime brackets it. }} {{#isDeepObject}} {{>apisAssignQueryParam}} {{/isDeepObject}} {{^isDeepObject}} {{#isMap}} for (let key of Object.keys(requestParameters['{{paramName}}'])) { - queryParameters[key] = (requestParameters['{{paramName}}'] as any)[key]; + const value = (requestParameters['{{paramName}}'] as any)[key]; + if (value != null) { + queryParameters[key] = value; + } } {{/isMap}} {{^isMap}} 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 3c599ace8713..34531b90198b 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 @@ -1177,7 +1177,7 @@ public void testDateFormatIsSerializedAsACalendarDate() throws IOException { TestUtils.assertFileContains(api, "formParams.append('createdAt', runtime.serializeDateTime(requestParameters['createdAt'] as any))"); } - @Test(description = "Verify an object query parameter is exploded, whether or not it declares its properties") + @Test(description = "Verify a form style, exploded map query parameter goes on the wire one entry per parameter") public void testExplodedObjectQueryParameter() throws IOException { File output = generate(new HashMap<>(), "src/test/resources/3_0/exploded-object-query-param.yaml"); Path api = Paths.get(output + "/apis/DefaultApi.ts"); @@ -1187,13 +1187,16 @@ public void testExplodedObjectQueryParameter() throws IOException { // through to a whole-object assignment and end up bracketed by the runtime. TestUtils.assertFileContains(api, "for (let key of Object.keys(requestParameters['filter'])) {", - "queryParameters[key] = (requestParameters['filter'] as any)[key];"); + "const value = (requestParameters['filter'] as any)[key];"); TestUtils.assertFileNotContains(api, "queryParameters['filter'] = requestParameters['filter'];"); // a declared map behaves the same way TestUtils.assertFileContains(api, "for (let key of Object.keys(requestParameters['typedFilter'])) {", - "queryParameters[key] = (requestParameters['typedFilter'] as any)[key];"); + "const value = (requestParameters['typedFilter'] as any)[key];"); + + // a null or undefined entry is left out; the runtime would send it as k=null or k=undefined + TestUtils.assertFileContains(api, "if (value != null) { queryParameters[key] = value; }"); // deepObject nests under the parameter name, which the runtime does for a whole object TestUtils.assertFileContains(api, "queryParameters['deepFilter'] = requestParameters['deepFilter'];"); diff --git a/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml b/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml index fd6fce7d5807..2f94dce6c7a6 100644 --- a/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml @@ -2,9 +2,7 @@ openapi: 3.0.3 info: title: Exploded object query parameters description: > - Object typed query parameters, covering the four combinations of style and explode that - decide how an object is put on the wire. The free-form variants matter because a - free-form object is flagged isMap but not isContainer. + Object typed query parameters under form/explode (as a free-form object and as a typed map), deepObject, and form without explode. The free-form variant matters because it is flagged isMap but not isContainer. version: 1.0.0 servers: - url: localhost:8080 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 07c04b2fd3da..1cb453dd2871 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 @@ -1560,7 +1560,10 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = (requestParameters['language'] as any)[key]; + const value = (requestParameters['language'] as any)[key]; + if (value != null) { + queryParameters[key] = value; + } } } 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 590f15518042..33782c80db99 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 @@ -1560,7 +1560,10 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = (requestParameters['language'] as any)[key]; + const value = (requestParameters['language'] as any)[key]; + if (value != null) { + queryParameters[key] = value; + } } } 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 aea78c920a9d..7a6315a8750d 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 @@ -1378,7 +1378,10 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = (requestParameters['language'] as any)[key]; + const value = (requestParameters['language'] as any)[key]; + if (value != null) { + queryParameters[key] = value; + } } } From a196b36e583632bc5e3277ce715803878e998cf3 Mon Sep 17 00:00:00 2001 From: Wiebren Braakman Date: Wed, 23 Sep 2026 11:33:05 +0200 Subject: [PATCH 3/5] fix: [typescript-fetch] keep an exploded entry named __proto__ Assigning queryParameters['__proto__'] hits the Object.prototype setter and drops the entry, while the runtime keeps such a key under deepObject and in nested objects. The loop now defines the property instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/main/resources/typescript-fetch/apis.mustache | 2 +- .../typescript/fetch/TypeScriptFetchClientCodegenTest.java | 4 ++-- .../typescript-fetch/builds/default-v3.0/apis/FakeApi.ts | 2 +- .../typescript-fetch/builds/kebab-case/apis/fake-api.ts | 2 +- .../builds/snakecase-discriminator/apis/FakeApi.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) 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 776ed362ee5a..036c2a1049ea 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -211,7 +211,7 @@ export class {{classname}} extends runtime.BaseAPI { for (let key of Object.keys(requestParameters['{{paramName}}'])) { const value = (requestParameters['{{paramName}}'] as any)[key]; if (value != null) { - queryParameters[key] = value; + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); } } {{/isMap}} 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 34531b90198b..4b54ec04eda0 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 @@ -1195,8 +1195,8 @@ public void testExplodedObjectQueryParameter() throws IOException { "for (let key of Object.keys(requestParameters['typedFilter'])) {", "const value = (requestParameters['typedFilter'] as any)[key];"); - // a null or undefined entry is left out; the runtime would send it as k=null or k=undefined - TestUtils.assertFileContains(api, "if (value != null) { queryParameters[key] = value; }"); + // a null or undefined entry is left out (the runtime would send k=null or k=undefined); defineProperty keeps a key named __proto__ + TestUtils.assertFileContains(api, "if (value != null) { Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); }"); // deepObject nests under the parameter name, which the runtime does for a whole object TestUtils.assertFileContains(api, "queryParameters['deepFilter'] = requestParameters['deepFilter'];"); 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 1cb453dd2871..824be8eef113 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 @@ -1562,7 +1562,7 @@ export class FakeApi extends runtime.BaseAPI { for (let key of Object.keys(requestParameters['language'])) { const value = (requestParameters['language'] as any)[key]; if (value != null) { - queryParameters[key] = value; + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); } } } 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 33782c80db99..fda7529f8dfd 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 @@ -1562,7 +1562,7 @@ export class FakeApi extends runtime.BaseAPI { for (let key of Object.keys(requestParameters['language'])) { const value = (requestParameters['language'] as any)[key]; if (value != null) { - queryParameters[key] = value; + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); } } } 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 7a6315a8750d..57d0e86a9a03 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 @@ -1380,7 +1380,7 @@ export class FakeApi extends runtime.BaseAPI { for (let key of Object.keys(requestParameters['language'])) { const value = (requestParameters['language'] as any)[key]; if (value != null) { - queryParameters[key] = value; + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); } } } From bf8483e2d6f31185c9fa926577f47a6cd2c3ea4e Mon Sep 17 00:00:00 2001 From: Wiebren Braakman Date: Wed, 23 Sep 2026 12:31:07 +0200 Subject: [PATCH 4/5] test: [typescript-fetch] say the null-skip assertion pins emitted code The codegen test cannot run the generated client, so its comment now says it locks the code shape; the wire behaviour was checked separately. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../typescript/fetch/TypeScriptFetchClientCodegenTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4b54ec04eda0..a612e628eff8 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 @@ -1195,7 +1195,7 @@ public void testExplodedObjectQueryParameter() throws IOException { "for (let key of Object.keys(requestParameters['typedFilter'])) {", "const value = (requestParameters['typedFilter'] as any)[key];"); - // a null or undefined entry is left out (the runtime would send k=null or k=undefined); defineProperty keeps a key named __proto__ + // pins the emitted loop: a null or undefined entry is skipped, and the entry is defined rather than assigned so a key named __proto__ survives TestUtils.assertFileContains(api, "if (value != null) { Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); }"); // deepObject nests under the parameter name, which the runtime does for a whole object From df655b6442fa4c62fa8909a3f2c9a843480dd3ec Mon Sep 17 00:00:00 2001 From: Esteban Gehring Date: Wed, 23 Sep 2026 16:13:20 +0200 Subject: [PATCH 5/5] Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../codegen/languages/TypeScriptFetchClientCodegen.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index b33bd47ed861..96d16b432d52 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -1559,7 +1559,14 @@ public ExtendedCodegenParameter(CodegenParameter cp) { this.isPrimitiveType = cp.isPrimitiveType; this.isModel = cp.isModel; this.isExplode = cp.isExplode; + this.isExplode = cp.isExplode; this.isDeepObject = cp.isDeepObject; + this.isFormStyle = cp.isFormStyle; + this.isMatrix = cp.isMatrix; + this.isAllowEmptyValue = cp.isAllowEmptyValue; + this.isSpaceDelimited = cp.isSpaceDelimited; + this.isPipeDelimited = cp.isPipeDelimited; + this.baseName = cp.baseName; this.paramName = cp.paramName; this.dataType = cp.dataType;