Skip to content

Commit 8932249

Browse files
wiebrenclaude
andauthored
[dart] fix: explode object query parameters (#24868)
* fix: [dart] explode object query parameters 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 * fix: [dart] repeat the key per element for a collection in an exploded 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> * chore: [dart] trim the exploded query comment 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> * test: [dart] keep the shared fixture byte-identical with the other ports Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 139b1e7 commit 8932249

3 files changed

Lines changed: 46 additions & 1 deletion

File tree

‎modules/openapi-generator/src/main/resources/dart2/api.mustache‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,23 @@ class {{{classname}}} {
6767
{{^required}}
6868
if ({{{paramName}}} != null) {
6969
{{/required}}
70+
{{#isMap}}
71+
{{#isExplode}}
72+
{{^isDeepObject}}
73+
// form style, explode: one query parameter per entry, keyed by the property name; a collection repeats the key per non-null element
74+
{{#isFreeFormObject}}({{{paramName}}} as Map){{/isFreeFormObject}}{{^isFreeFormObject}}{{{paramName}}}{{/isFreeFormObject}}.forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));
75+
{{/isDeepObject}}
76+
{{#isDeepObject}}
7077
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
78+
{{/isDeepObject}}
79+
{{/isExplode}}
80+
{{^isExplode}}
81+
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
82+
{{/isExplode}}
83+
{{/isMap}}
84+
{{^isMap}}
85+
queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}}));
86+
{{/isMap}}
7187
{{^required}}
7288
}
7389
{{/required}}

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientCodegenTest.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,34 @@ public void testObjectArrayDoesNotUseListFromJson() throws Exception {
137137
TestUtils.assertFileContains(modelFile.toPath(), "cast<Object>");
138138
}
139139

140+
@Test(description = "Verify a form style, exploded map query parameter goes on the wire one entry per parameter")
141+
public void testExplodedObjectQueryParameter() throws Exception {
142+
List<File> files = generateDartNativeFromSpec(
143+
"src/test/resources/3_0/exploded-object-query-param.yaml");
144+
145+
File apiFile = files.stream()
146+
.filter(f -> f.getName().equals("default_api.dart"))
147+
.findFirst()
148+
.orElseThrow(() -> new AssertionError("default_api.dart not found in generated files"));
149+
150+
// form style with explode - the default - puts every entry on the wire under its own
151+
// property name. Handing the whole map to _queryParams stringifies it with
152+
// Map.toString(), which is what used to happen. A collection value repeats the key per
153+
// non-null element ('multi').
154+
TestUtils.assertFileContains(apiFile.toPath(),
155+
"(filter as Map).forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));");
156+
TestUtils.assertFileNotContains(apiFile.toPath(), "_queryParams('', 'filter', filter)");
157+
158+
// a declared map behaves the same way, and needs no cast
159+
TestUtils.assertFileContains(apiFile.toPath(),
160+
"typedFilter.forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));");
161+
162+
// deepObject and form without explode both keep a single parameter
163+
TestUtils.assertFileContains(apiFile.toPath(),
164+
"_queryParams('', 'deepFilter', deepFilter)",
165+
"_queryParams('', 'flatFilter', flatFilter)");
166+
}
167+
140168
@Test(description = "Enum properties with defaults should emit enum constructor, not string literal")
141169
public void testEnumDefaultUsesEnumConstructor() throws Exception {
142170
List<File> files = generateDartNativeFromSpec(

‎samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/api/fake_api.dart‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,8 @@ class FakeApi {
13821382
queryParams.addAll(_queryParams('csv', 'url', url));
13831383
queryParams.addAll(_queryParams('multi', 'context', context));
13841384
if (language != null) {
1385-
queryParams.addAll(_queryParams('', 'language', language));
1385+
// form style, explode: one query parameter per entry, keyed by the property name; a collection repeats the key per non-null element
1386+
language.forEach((entryKey, dynamic entryValue) => queryParams.addAll(_queryParams('multi', entryKey.toString(), entryValue is Iterable ? entryValue.where((e) => e != null).toList() : entryValue)));
13861387
}
13871388
queryParams.addAll(_queryParams('', 'allowEmpty', allowEmpty));
13881389

0 commit comments

Comments
 (0)