Conversation
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.
|
Split per language as requested, out of #24797. This is the typescript-fetch half; the
The three touch disjoint sets of files under Each branch was tested on its own after the split, not just as part of the original combined |
|
cubic came back clean on this PR, but its earlier run against the pre-split commit on #24797 Declared object models still serialized whole — valid, pre-existing, deliberately out of
const src = JSON.parse('{"__proto__":"x","category":"books"}');
const queryParameters = {};
for (const key of Object.keys(src)) queryParameters[key] = src[key];
Object.keys(queryParameters); // ['category'] — the __proto__ entry is goneThe assignment hits the prototype setter instead of creating an own property, so the entry |
|
thanks for the PR cc @TiFu (2017/07) @taxpon (2017/07) @sebastianhaas (2017/07) @kenisteward (2017/07) @Vrolijkx (2017/09) @macjohnny (2018/01) @topce (2018/10) @akehir (2019/07) @petejohansonxo (2019/11) @amakhrov (2020/02) @davidgamero (2022/03) @mkusaka (2022/04) @joscha (2024/10) @KannaKim (2026/07) |
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) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache:214">
P2: This assignment drops duplicate query names when an exploded object key collides with another parameter. Accumulate existing values into an array so the runtime emits both query occurrences.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| queryParameters[key] = requestParameters['{{paramName}}'][key]; | ||
| const value = (requestParameters['{{paramName}}'] as any)[key]; | ||
| if (value != null) { | ||
| queryParameters[key] = value; |
There was a problem hiding this comment.
P2: This assignment drops duplicate query names when an exploded object key collides with another parameter. Accumulate existing values into an array so the runtime emits both query occurrences.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache, line 214:
<comment>This assignment drops duplicate query names when an exploded object key collides with another parameter. Accumulate existing values into an array so the runtime emits both query occurrences.</comment>
<file context>
@@ -202,14 +202,23 @@ export class {{classname}} extends runtime.BaseAPI {
- queryParameters[key] = requestParameters['{{paramName}}'][key];
+ const value = (requestParameters['{{paramName}}'] as any)[key];
+ if (value != null) {
+ queryParameters[key] = value;
+ }
}
</file context>
There was a problem hiding this comment.
This is a known gap, listed in the description: an exploded key equal to a declared parameter's name overwrites it, or is overwritten, depending on emit order. The runtime would repeat the key for an array value, but every declared query parameter is a plain queryParameters[name] = … assignment in apis.mustache, so keeping both means changing all of those sites to merge, not just this loop. That is a wider change than this PR; the kotlin port keeps both because its query map is multi-valued throughout.
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) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
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) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Bug
A query parameter whose schema is an object, with
style/explodeleft at their defaults (form,true), must go on the wire as one parameter per entry, keyed by the property name alone. Thetypescript-fetchclient bracketed it instead.called with
{"category": "books", "createdDate:gte": "2023-01-01"}:category=books&createdDate%3Agte=2023-01-01filter[category]=books&filter[createdDate%3Agte]=2023-01-01Series
One of six per-language PRs for the same bug: #24797 go, #24802 python, #24803 typescript-fetch, #24867 kotlin, #24868 dart, #24869 ruby. No shared
main/code; each adds the same fixture,3_0/exploded-object-query-param.yaml.Fix
apis.mustachegates the explode loop onisMap, notisContainer. A baretype: objectisisMapbut notisContainer(only anadditionalPropertiesmap is), so it was assigned whole and the runtime bracketed it.TypeScriptFetchClientCodegencopiesisDeepObject.ExtendedCodegenParameter's copy constructor dropped it, so it was always false in the templates. A deepObject parameter isexplode: trueas well, and now stays assigned whole so the runtime brackets it.as anyon the indexing. A free-form object is typedobject, andobject[key]isTS7053understrict.{k: null}ask=nulland{k: undefined}ask=undefined.queryParameters['__proto__'] = vhits the prototype setter and drops the entry;Object.definePropertykeeps it (__proto__=x), as the runtime already does under deepObject.key=value); it now gets its brackets.Verified
A client generated from the fixture, recording the URL it hands to
fetch:filter: {category: 'books', 'createdDate:gte': '2023-01-01'}category=books&createdDate%3Agte=2023-01-01typedFilter: {category: 'books'}category=booksfilter: {k: null, u: undefined, a: 'b', l: ['x', 2]}a=b&l=x&l=2(before the null skip:k=null&u=undefined&a=b&l=x&l=2)deepFilter: {category: 'books'}deepFilter%5Bcategory%5D=books(as on master)flatFilter: {category: 'books'}flatFilter%5Bcategory%5D=books(as on master)TypeScriptFetchClientCodegenTest#testExplodedObjectQueryParameterfails without the fix.Known gaps
$refed object model isisModel, so it still goes on the wire whole. Its interface properties aren't the wire names (_floatforfloat, aDatefordateTime), so a fix has to go through{{dataType}}ToJSONfirst.explode: falseis still bracketed; the spec wantsflatFilter=category,books.queryParametersobject as the declared parameters, so an entry and a declared parameter with the same name overwrite each other, depending on emit order. Keeping both would mean merging at every query assignment inapis.mustache, not just this loop.null.PR checklist
./bin/generate-samples.sh bin/configs/typescript-fetch*.yaml): 3FakeApi.tsfiles, the cast and the null skip on the petstore'slanguagemap.Summary by cubic
Fixes the
typescript-fetchclient so object query parameters with defaultform/explodego on the wire as one parameter per entry, keyed by the property name alone (category=books), instead of bracketed under the parameter name (filter[category]=books). deepObject andexplode: falseobjects keep their previous wire format.Bug Fixes
isContainer, which free-form objects never set; it now keys onisMap, with anas anycast so strict TypeScript still compiles.k=null/k=undefined.ExtendedCodegenParameter's copy constructor dropped style-related flags, breaking deepObject parameters; it now copiesisDeepObject,isFormStyle,isMatrix,isAllowEmptyValue,isSpaceDelimited, andisPipeDelimited. A duplicatethis.isExplode = cp.isExplode;line from the latest commit should be removed.Known gap
$refed object model as a query parameter still goes on the wire as a whole; fixing it requires routing through the model'sToJSONfor wire-name translation, which is out of scope.Written for commit df655b6. Summary will update on new commits.
Generated with Claude Code