Skip to content

Commit a61e05c

Browse files
wiebrenclaude
andauthored
[go] fix: explode object query parameters (#24797)
* fix: [go] 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 go client bracketed it as filter[category]=books instead. parameterAddToHeaderOrQuery in go/client.mustache bracketed every map whatever style it was handed. The reflect.Slice case immediately above it already branches on style == "deepObject", so the map case looks like an oversight. The generated api passes the style correctly; nothing downstream read it. The flattening applies to the top level only. The recursive call no longer carries the style down, so anything nested inside an entry keeps the path it has accumulated: {profile: {name: "x"}} is profile[name]=x, and two siblings holding the same property name stay distinct — {a: {x: 1}, b: {x: 2}} is a[x]=1&b[x]=2 rather than a pair of colliding x= entries. Nested slices are unaffected, since the slice branch only special cases deepObject and so behaves identically for an empty style and for form. * fix: [go] an array element does not inherit the form flattening A form style query parameter whose value is an array of maps reached the map branch one level down with the style still set, so the entries were keyed by their property names alone and the accumulated parent path was dropped. Reset the style when descending into array elements, mirroring the reset the map branch already does for its entries: only a map that is the parameter's direct value is exploded to bare property names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Go3AyndcGwv5tFTwo9aBfy * test: [go] cover the array of objects the element reset exists for The fixture declared four object parameters and no array, so the assertions added for the element reset in 470b7e0 only matched template text that every generated client.go carries: they held whatever the fixture contained. Add the array of objects, and deliberately not exploded. That is the only shape that reaches the array branch of parameterAddToHeaderOrQuery: with explode: true the api template unrolls the slice itself and hands each element over as a map, which enters the map branch instead, so an exploded array would have left the branch as uncovered as no array at all. Not exploded, the whole slice arrives as one form style value: parameterAddToHeaderOrQuery(localVarQueryParams, "arrayFilter", r.arrayFilter, "form", "csv") which is asserted, and is derived from the spec rather than from the template. The behaviour itself lives in generated go rather than in the generator, so it is not reachable from a codegen test. Exercising the helper in a generated client directly, with a []map[string]interface{} under style form: filter[a]=1 with the reset, a=1 without it, while a map that is the parameter's own value stays a=1 either way. mvn -pl modules/openapi-generator -Dtest=GoClientCodegenTest test: 28 tests, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01894FEkx3sDA5wTqjeEo33D * chore: [go] drop the stray go.mod and go.sum changes Running the go toolchain in the issue_20079_go_regex_wrongly_translated sample rewrote its go.mod and go.sum, and the rewrite was swept into the array element commit. bin/generate-samples.sh emits neither, so the samples up to date job reverted both files and failed on the dirty tree. Restore them to what the generator produces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JHEXQ9zWNyGwVnWPJJnzSo * fix: [go] skip nil entries of an exploded object query parameter A nil entry of a form style exploded map went on the wire as the literal "null" (nullValue=null), and so did a nil element of a list an entry holds (anyList=x&anyList=null&anyList=2). Both are now left out, after unwrapping interfaces and pointers, matching the kotlin port. A typed nil pointer, such as (*string)(nil), panicked with "reflect.Value.Interface on zero Value" because the pointer branch called Elem().Interface() without a nil check. It is now left out as well; that branch is shared with deepObject, where it turns the same panic into an omitted value. deepObject is otherwise unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: [go] skip nil entries under deepObject too The previous commit left nil entries out of a form style exploded map but not out of a deepObject one, so the same map went on the wire two ways: a nil entry was dropped under form and sent as filter[k]=null under deepObject. deepObject was also inconsistent with itself, because the nil check added to the pointer branch already dropped a typed nil pointer there. Move the entry check ahead of the form branch, so it applies whatever the style, and skip a nil element in the array branch as well. The elements that survive keep the index they had, so filter[l][0] and filter[l][2] stay where they are rather than closing the gap; the index names a position in the array the caller passed, and renumbering would quietly move the remaining values. Nothing changes for a non-nil value, and a nil parameter passed at the top level still goes out as key=null. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * revert: [go] keep deepObject's null for a nil entry This reverts cd901bc. deepObject sending [key]=null for a nil entry is behaviour the petstore sample pins in TestQueryDeepObject, and it is the same on master, so it stays out of a change about form style. Form style keeps skipping nil entries, and the nil check in the pointer case stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: [go] pin the nil-entry skip inside the form branch The assertion on parameterValueIndirect alone also matched the reverted layout that skipped nil for every style. Pinning what follows the skip fails if it is ever moved ahead of the style branch again, which would break deepObject's [key]=null that TestQueryDeepObject expects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: [go] trim the exploded object comments, test and fixture The shipped comments shrink to one line each, parameterValueIndirect returns true once it has unwrapped a non-nil value, the codegen test keeps only the assertions that fail without each fix, and arrayFilter leaves the shared fixture so it is byte-identical across the ports again. The fixture's description now names the three combinations it covers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: [go] keep the validity check in parameterValueIndirect The constant true reported an invalid reflect.Value as usable. No caller passes one today, but the check guards v.Interface() against a panic and costs nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent da04b93 commit a61e05c

14 files changed

Lines changed: 562 additions & 26 deletions

File tree

‎modules/openapi-generator/src/main/resources/go/client.mustache‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,14 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri
191191
for i:=0;i<lenIndValue;i++ {
192192
var arrayValue = indValue.Index(i)
193193
var keyPrefixForCollectionType = keyPrefix
194+
var styleForElement = style
194195
if style == "deepObject" {
195196
keyPrefixForCollectionType = keyPrefix + "[" + strconv.Itoa(i) + "]"
197+
} else if style == "form" {
198+
// only the parameter's own map is flattened; a map inside an array keeps its bracketed path
199+
styleForElement = ""
196200
}
197-
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), style, collectionType)
201+
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), styleForElement, collectionType)
198202
}
199203
return
200204
@@ -206,13 +210,36 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri
206210
iter := indValue.MapRange()
207211
for iter.Next() {
208212
k,v := iter.Key(), iter.Value()
209-
parameterAddToHeaderOrQuery(headerOrQueryParams, fmt.Sprintf("%s[%s]", keyPrefix, k.String()), v.Interface(), style, collectionType)
213+
var keyPrefixForMapEntry = fmt.Sprintf("%s[%s]", keyPrefix, k.String())
214+
var styleForMapEntry = style
215+
if style == "form" {
216+
// form style: one query parameter per entry, keyed by the property name; anything nested keeps its bracketed path
217+
keyPrefixForMapEntry = k.String()
218+
styleForMapEntry = ""
219+
// a nil entry, or a nil item of a list entry, is left out rather than sent as "null"
220+
entry, ok := parameterValueIndirect(v)
221+
if !ok {
222+
continue
223+
}
224+
if entry.Kind() == reflect.Slice {
225+
for i := 0; i < entry.Len(); i++ {
226+
if element, ok := parameterValueIndirect(entry.Index(i)); ok {
227+
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForMapEntry, element.Interface(), styleForMapEntry, collectionType)
228+
}
229+
}
230+
continue
231+
}
232+
}
233+
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForMapEntry, v.Interface(), styleForMapEntry, collectionType)
210234
}
211235
return
212236
213237
case reflect.Interface:
214238
fallthrough
215239
case reflect.Ptr:
240+
if v.IsNil() {
241+
return
242+
}
216243
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, v.Elem().Interface(), style, collectionType)
217244
return
218245
@@ -247,6 +274,18 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri
247274
}
248275
}
249276
277+
// parameterValueIndirect unwraps interfaces and pointers down to the value they hold,
278+
// reporting false when that value is nil
279+
func parameterValueIndirect(v reflect.Value) (reflect.Value, bool) {
280+
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
281+
if v.IsNil() {
282+
return v, false
283+
}
284+
v = v.Elem()
285+
}
286+
return v, v.IsValid()
287+
}
288+
250289
// helper for converting interface{} parameters to json strings
251290
func parameterToJson(obj interface{}) (string, error) {
252291
jsonBuf, err := json.Marshal(obj)

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoClientCodegenTest.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,35 @@ public void testPrimitiveTypeInOneOf() throws IOException {
141141
TestUtils.assertFileNotContains(modelFile, "dst.int32");
142142
}
143143

144+
@Test(description = "Verify form style query parameters explode an object instead of bracketing it")
145+
public void testExplodedObjectQueryParameter() throws IOException {
146+
File output = Files.createTempDirectory("test").toFile();
147+
output.deleteOnExit();
148+
149+
final CodegenConfigurator configurator = new CodegenConfigurator()
150+
.setGeneratorName("go")
151+
.setInputSpec("src/test/resources/3_0/exploded-object-query-param.yaml")
152+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
153+
154+
DefaultGenerator generator = new DefaultGenerator();
155+
List<File> files = generator.opts(configurator.toClientOptInput()).generate();
156+
files.forEach(File::deleteOnExit);
157+
158+
TestUtils.assertFileContains(Paths.get(output + "/client.go"),
159+
"keyPrefixForMapEntry = k.String()",
160+
"if !ok { continue } if entry.Kind() == reflect.Slice {",
161+
"case reflect.Ptr: if v.IsNil() { return }",
162+
"styleForElement = \"\"");
163+
164+
// the api passes the declared style through
165+
Path api = Paths.get(output + "/api_default.go");
166+
TestUtils.assertFileContains(api,
167+
"parameterAddToHeaderOrQuery(localVarQueryParams, \"filter\", r.filter, \"form\", \"\")",
168+
"parameterAddToHeaderOrQuery(localVarQueryParams, \"typedFilter\", r.typedFilter, \"form\", \"\")",
169+
"parameterAddToHeaderOrQuery(localVarQueryParams, \"deepFilter\", r.deepFilter, \"deepObject\", \"\")",
170+
"parameterAddToHeaderOrQuery(localVarQueryParams, \"flatFilter\", r.flatFilter, \"form\", \"\")");
171+
}
172+
144173
@Test
145174
public void testNullableComposition() throws IOException {
146175
File output = Files.createTempDirectory("test").toFile();

‎samples/client/echo_api/go-external-refs/client.go‎

Lines changed: 41 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎samples/client/echo_api/go/client.go‎

Lines changed: 41 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎samples/client/others/go/allof_multiple_ref_and_discriminator/client.go‎

Lines changed: 41 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)