Skip to content

[dart] fix: explode object query parameters - #24868

Open
wiebren wants to merge 1 commit into
OpenAPITools:masterfrom
wiebren:fix/exploded-object-query-parameters-dart
Open

[dart] fix: explode object query parameters#24868
wiebren wants to merge 1 commit into
OpenAPITools:masterfrom
wiebren:fix/exploded-object-query-parameters-dart

Conversation

@wiebren

@wiebren wiebren commented Sep 4, 2026

Copy link
Copy Markdown

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 dart client stringified the whole map with
Map.toString() into a single parameter instead, while csharp, java and php got it
right.

Given:

parameters:
  - in: query
    name: filter
    schema:
      type: object

called with {"tld": "com", "createdDate:gte": "2023-01-01"}:

generator on the wire
expected tld=com&createdDate%3Agte=2023-01-01
dart filter=%7Btld%3A+com%2C+createdDate%3Agte%3A+2023-01-01%7D — dart's Map.toString(), percent encoded
csharp, java, php correct

Sibling of #24797 (go), #24802 (python) and #24803 (typescript-fetch), one PR per language
per the maintainer's request on #24797. This PR is independent of the other three — no
shared main/ code. It adds the same test fixture at the same path with identical content,
so whichever lands first, the others rebase cleanly.

The cause

dart2/api.mustache — every query parameter was handed whole to _queryParams, which
receives only a collection format (empty for a map) and falls through to
parameterToString(value), i.e. value.toString(). No style or explode reaches the
runtime.

The fix mirrors the accepted python change in #24802: the generated api iterates an exploded
map entry by entry at the call site —

(filter as Map).forEach((entryKey, entryValue) => queryParams.addAll(_queryParams('', entryKey.toString(), entryValue)));

— and _queryParams is left alone, since it is shared by every call site and is still the
right fallback for a parameter that is not an exploded map. A free-form object is typed
Object in dart and needs the cast; a declared map does not get one, so the analyzer stays
clean (dart analyze on the generated fixture client: no issues). deepObject and
explode: false parameters keep the exact line they produced before.

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
request target back:

parameter before after
filter (object, defaults) filter=%7Btld%3A+com%2C+createdDate%3Agte%3A+2023-01-01%7D tld=com&createdDate%3Agte=2023-01-01
typedFilter (map, defaults) same Map.toString() shape tld=com&createdDate%3Agte=2023-01-01
deepFilter (style: deepObject) deepFilter=%7Btld%3A+com%2C…%7D unchanged, byte for byte
flatFilter (explode: false) flatFilter=%7Btld%3A+com%2C…%7D unchanged, byte for byte

Known gaps, called out deliberately

Declared object models. A $refed object model as a query parameter is isModel, not
isMap, so it still goes on the wire whole. Pre-existing, and the same gap the sibling PRs
document — iterating a model would put the dart property names on the wire rather than the
wire names. Left for a follow-up.

deepObject and explode: false keep their previous single-Map.toString() parameter.
Both are wrong per the spec (deepFilter[tld]=com and flatFilter=tld,com,… respectively),
both were wrong before this change, and both go on the wire byte for byte as they did
before — same scoping as the sibling PRs.

A non-map value in a free-form object parameter now throws a TypeError at the cast
instead of going on the wire as its toString(). This matches python, where a non-dict
raises at .items().

Tests

modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yaml covers
the four combinations that decide the wire format:

  • DartClientCodegenTest#testExplodedObjectQueryParameter

It fails without the fix (verified by stashing only the template change). All 13 tests in
DartClientCodegenTest pass.

PR checklist

  • Read the contribution guidelines.
  • Built the project and updated samples (./bin/generate-samples.sh for
    bin/configs/dart-*.yaml; ./bin/utils/export_docs_generators.sh produced no diff).
    1 sample file changed: petstore_client_lib_fake/lib/api/fake_api.dart, from the
    petstore fixture's language parameter — a declared map with the default style, the
    same parameter that changed in the python sibling. The dart-dio samples are untouched,
    since that generator has its own templates.
  • Technical committee: @yissachar @joernahrens @swipesight @jaumard

Generated with Claude Code


Summary by cubic

Fixes the Dart client so object query parameters with the default style: form and explode: true go on the wire as one parameter per entry, keyed by property name, instead of as a single Map.toString()-stringified parameter.

Behavior

  • Free-form objects and declared maps are now iterated entry by entry at the generated call site.
  • deepObject and explode: false parameters keep their previous wire format, byte for byte.
  • A non-map value in a free-form object now throws a TypeError at the cast instead of being stringified.

Known gaps

  • $refed object models as query parameters are still sent whole; pre-existing behavior left for a follow-up.
  • deepObject and explode: false output remains spec-incorrect but unchanged from before.

Written for commit bbcc510. Summary will update on new commits.

Review in cubic

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 dart client handed
the whole map to _queryParams, which stringified it with Map.toString() into a
single parameter (filter={tld: com, createdDate:gte: 2023-01-01}).

The generated api now iterates an exploded map entry by entry at the call
site; _queryParams is left alone, since it never receives style or explode
and is still the right fallback for every other parameter. A free-form
object is typed Object in dart and needs a cast to Map; a declared map does
not. deepObject and explode: false objects keep their previous wire format,
byte for byte.

The new test fixture covers the four style/explode combinations that decide
the wire format; the test fails without the template change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxDNCjqJycKTfzVWg2SeTJ

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant