Skip to content

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

Merged
wing328 merged 4 commits into
OpenAPITools:masterfrom
wiebren:fix/exploded-object-query-parameters-dart
Sep 24, 2026
Merged

wing328 merged 4 commits into
OpenAPITools:masterfrom
wiebren:fix/exploded-object-query-parameters-dart

Conversation

@wiebren

@wiebren wiebren commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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 dart client stringified the whole map with Map.toString() into a single parameter 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
dart before filter=%7Bcategory%3A+books%2C+createdDate%3Agte%3A+2023-01-01%7D

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

dart2/api.mustache handed every query parameter whole to _queryParams, which knows only a collection format and falls through to toString(). Like the python port (#24802), the generated api now iterates an exploded map entry by entry at the call site; _queryParams itself is untouched.

  • A free-form object is typed Object and gets a Map cast; a declared map needs none (dart analyze on the generated fixture client: no issues).
  • Each entry goes through _queryParams with 'multi', after dropping null items and turning any Iterable into a List, so the key repeats per element (tld=com&tld=net).
  • A null entry is left out, as _queryParams already did.
  • deepObject and explode: false parameters produce the line they did before.

Verified

A client generated from the fixture, against a server that echoes its raw query string:

input on the wire
filter or typedFilter with {"category": "books", "createdDate:gte": "2023-01-01"} category=books&createdDate%3Agte=2023-01-01
filter with {"anyList": ["x", null, 2]} anyList=x&anyList=2
declared page=declared plus filter with {"page": "fromMap"} page=declared&page=fromMap (both kept)
deepFilter (deepObject) or flatFilter (explode: false) Map.toString(), as on master (see Known gaps)

DartClientCodegenTest#testExplodedObjectQueryParameter fails without the fix.

Known gaps

  • Declared object models ($ref, isModel rather than isMap) still go on the wire whole; iterating one would send the dart property names rather than the wire names.
  • deepObject and explode: false keep their single Map.toString() parameter, as on master. Both are wrong per the spec (deepFilter[category]=books, flatFilter=category,books,…).
  • Nested objects inside an exploded map go out as their toString(); form style defines no encoding for them.
  • A non-map value in a free-form object parameter now throws a TypeError at the cast instead of going out as its toString(), as python raises for a non-dict.

PR checklist

  • Read the contribution guidelines.
  • Built the project and updated samples (./bin/generate-samples.sh bin/configs/dart*.yaml): 1 file, petstore_client_lib_fake/lib/api/fake_api.dart, from the petstore fixture's language parameter (a declared map with the default style). The dart-dio samples have their own templates and are untouched.
  • Technical committee: @yissachar @joernahrens @swipesight @jaumard

Generated with Claude Code

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

…d object

An exploded form-style object query parameter handed each entry to
_queryParams with an empty collection format, which defaults to csv, so a
list value went out comma joined (tld=com%2Cnet) instead of once per element
(tld=com&tld=net). A Set went out as its toString(), since _queryParams only
expands a List, and a null element went out as an empty value.

Each entry is now passed with the 'multi' collection format, and an Iterable
value is turned into a List with its null elements dropped first, so the key
repeats once per non-null element and an empty collection adds nothing. The
closure parameter is typed dynamic so the Iterable check still compiles for a
map with a declared value type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

1 issue found across 4 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/dart2/api.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/dart2/api.mustache:71">
P2: When `explode` is omitted for an object query parameter, this template does not enter the new per-entry branch. Update codegen metadata to represent the OpenAPI form-object default as exploded, while preserving explicit `explode: false`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if ({{{paramName}}} != null) {
{{/required}}
{{#isMap}}
{{#isExplode}}

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.

P2: When explode is omitted for an object query parameter, this template does not enter the new per-entry branch. Update codegen metadata to represent the OpenAPI form-object default as exploded, while preserving explicit explode: false.

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/dart2/api.mustache, line 71:

<comment>When `explode` is omitted for an object query parameter, this template does not enter the new per-entry branch. Update codegen metadata to represent the OpenAPI form-object default as exploded, while preserving explicit `explode: false`.</comment>

<file context>
@@ -67,7 +67,24 @@ class {{{classname}}} {
     if ({{{paramName}}} != null) {
           {{/required}}
+        {{#isMap}}
+          {{#isExplode}}
+            {{^isDeepObject}}
+      // form style explodes an object into one query parameter per entry, keyed by the property name alone;
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The default is already applied upstream of this template: swagger-parser fills in explode for form style (OpenAPIDeserializer sets explode = true when the style is form and the spec leaves it out), so isExplode is true by the time the template runs.

Checked with a jar built from this branch: a parameter with neither style nor explode, and one with only style: form, both generate the per-entry forEach branch. Explicit explode: false stays a single parameter.

wiebren and others added 2 commits September 23, 2026 08:59
The shipped comment is one line now, without the implementation note.
Fix the fixture description, which claimed four style/explode
combinations where it covers three, and retitle the test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@wing328
wing328 merged commit 8932249 into OpenAPITools:master Sep 24, 2026
16 checks passed
@wing328 wing328 added this to the 7.26.0 milestone Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants