[typescript-fetch] fix: explode object query parameters - #24803
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 |
A query parameter whose schema is an object and whose
style/explodeare left at theirdefaults —
style: form,explode: true— must go on the wire as one parameter per entry,keyed by the property name alone. The
typescript-fetchclient bracketed it instead, whilecsharp,javaandphpgot it right.Given:
called with
{"category": "books", "createdDate:gte": "2023-01-01"}:category=books&createdDate%3Agte=2023-01-01filter[category]=books&filter[createdDate%3Agte]=2023-01-01Two causes
1.
typescript-fetch/apis.mustache— the explode branch was gated onisContainer,which
DefaultCodegen.fromParameteronly sets viaupdateParameterForMap, which in turnneeds
ModelUtils.isMapSchema(so,additionalProperties). A baretype: objectinsteadgoes through
setTypeProperties, which setsisMapandisFreeFormObjectand leavesisContainerfalse. The parameter was therefore assigned whole and the runtime'squerystringSingleKeybracketed it. Now keyed onisMap; inside{{^isArray}},isContainerimpliesisMap, so this is the same condition plus free-form objects.2.
TypeScriptFetchClientCodegen—ExtendedCodegenParameter's copy constructor copiesisExplodeandstylebut notisDeepObject,isFormStyle,isMatrix,isAllowEmptyValue,isSpaceDelimitedorisPipeDelimited, so all six were false in everytypescript-fetch template regardless of the document.
--global-property debugOperations=trueon a parameter declaringstyle: deepObjectshows"isDeepObject": falsetwo lines above"style": "deepObject". This is independent of (1), and it is whyfixing (1) alone would have broken deepObject parameters — they are
explode: truetoo.Verified on the wire, not just asserted
The client was generated from the new fixture and pointed at a server that echoes its own raw
query string back:
filter(object, defaults)category=books&createdDate%3Agte=2023-01-01typedFilter(map, defaults)deepFilter(style: deepObject)deepFilter%5Bcategory%5D=books&…flatFilter(explode: false)flatFilter%5Bcategory%5D=books&…The first two rows are the fixed behaviour; the last two are byte-for-byte what the generator
did before.
Known gap, called out deliberately
Declared object models. A
$refed object model as a query parameter isisModel, notisMap, so it still goes on the wire whole. This is pre-existing. It cannot be fixed bywidening the
isMapbranch: typescript-fetch interface properties are not the wire names,which is what the
ToJSONfunctions exist to translate —FormatTest.tsmaps'float'tovalue['_float']and'pattern_with_digits'tovalue['patternWithDigits'], anddateTimeneeds
serializeDateTime. IteratingObject.keysover a model would put_float=…and a rawDateon the wire. A correct fix has to route through{{dataType}}ToJSONfirst, which is afeature rather than a template branch.
Note on the diff
The loop body gains an
as anycast on the indexing. It is not needed for a declared map,but a free-form object is typed
objectandobject[key]iserror TS7053understrict,which is how most consumers compile. Checked both ways against the generated fixture client:
without the cast, one TS7053; with it, zero errors. That cast is the entire diff in the three
existing typescript-fetch petstore samples.
Tests
modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yamlcoversthe four combinations that decide the wire format:
TypeScriptFetchClientCodegenTest#testExplodedObjectQueryParameterIt fails without the fixes (verified by stashing only the
main/changes). 67 tests passacross
TypeScriptFetchClientCodegenTest,TypeScriptFetchModelTestandTypeScriptFetchClientOptionsTest.PR checklist
./bin/generate-samples.shforbin/configs/typescript-fetch*.yaml;./bin/utils/export_docs_generators.shproducedno diff). 3 sample files changed, all the
as anycast above, from the petstorefixture's
languageparameter — a declared map with the default style.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, covering declared maps and free-form objects, with anas anycast so strict TypeScript still compiles.ExtendedCodegenParameter's copy constructor dropped six style-related flags, soisDeepObjectand friends were always false in templates; they're now copied, which keeps deepObject parameters intact.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 0dec118. Summary will update on new commits.
Generated with Claude Code