[typescript-fetch] fix: explode object query parameters
Bug
A query parameter whose schema is an object, with style/explode left at their defaults (form, true), must go on the wire as one parameter per entry, keyed by the property name alone. The typescript-fetch client bracketed it instead.
parameters:
- in: query
name: filter
schema:
type: object
called with {"category": "books", "createdDate:gte": "2023-01-01"}:
| on the wire | |
|---|---|
| expected | category=books&createdDate%3Agte=2023-01-01 |
| typescript-fetch before | filter[category]=books&filter[createdDate%3Agte]=2023-01-01 |
Series
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.- A null entry is left out. The runtime sent
{k: null}ask=nulland{k: undefined}ask=undefined. - An entry is defined, not assigned.
queryParameters['__proto__'] = vhits the prototype setter and drops the entry;Object.definePropertykeeps it (__proto__=x), as the runtime already does under deepObject. - Side effect: a deepObject parameter typed as a map was flattened on master (
key=value); it now gets its brackets.
Verified
A client generated from the fixture, recording the URL it hands to fetch:
| input | on the wire |
|---|---|
filter: {category: 'books', 'createdDate:gte': '2023-01-01'} | category=books&createdDate%3Agte=2023-01-01 |
typedFilter: {category: 'books'} | category=books |
filter: {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#testExplodedObjectQueryParameter fails without the fix.
Known gaps
- Models. A
$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.- Name collisions. An entry goes into the same
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. - A null item of a list entry still goes out as
null.
PR checklist
- Read the contribution guidelines.
- Built the project and updated samples (
./bin/generate-samples.sh bin/configs/typescript-fetch*.yaml): 3FakeApi.tsfiles, the cast and the null skip on the petstore'slanguagemap. - Technical committee: @TiFu @macjohnny @topce @akehir @amakhrov @davidgamero @joscha
Summary by cubic
Fixes the typescript-fetch client so object query parameters with default form/explode go 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 and explode: false objects keep their previous wire format.
Bug Fixes
- The explode branch was gated on
isContainer, which free-form objects never set; it now keys onisMap, with anas anycast so strict TypeScript still compiles. - Null or undefined entries are now left off the wire instead of being sent as
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.- Adds a test fixture covering the four style/explode combinations.
Known gap
- A
$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.
Generated with Claude Code