[typescript-fetch] fix: explode object query parameters

Merged
WI
18d ago
+113-8

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
expectedcategory=books&createdDate%3Agte=2023-01-01
typescript-fetch beforefilter[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.mustache gates the explode loop on isMap, not isContainer. A bare type: object is isMap but not isContainer (only an additionalProperties map is), so it was assigned whole and the runtime bracketed it.
  • TypeScriptFetchClientCodegen copies isDeepObject. ExtendedCodegenParameter's copy constructor dropped it, so it was always false in the templates. A deepObject parameter is explode: true as well, and now stays assigned whole so the runtime brackets it.
  • as any on the indexing. A free-form object is typed object, and object[key] is TS7053 under strict.
  • A null entry is left out. The runtime sent {k: null} as k=null and {k: undefined} as k=undefined.
  • An entry is defined, not assigned. queryParameters['__proto__'] = v hits the prototype setter and drops the entry; Object.defineProperty keeps 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:

inputon 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 is isModel, so it still goes on the wire whole. Its interface properties aren't the wire names (_float for float, a Date for dateTime), so a fix has to go through {{dataType}}ToJSON first.
  • explode: false is still bracketed; the spec wants flatFilter=category,books.
  • Name collisions. An entry goes into the same queryParameters object 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 in apis.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): 3 FakeApi.ts files, the cast and the null skip on the petstore's language map.
  • 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 on isMap, with an as any cast 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 copies isDeepObject, isFormStyle, isMatrix, isAllowEmptyValue, isSpaceDelimited, and isPipeDelimited. A duplicate this.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's ToJSON for wire-name translation, which is out of scope.

Generated with Claude Code