Skip to content

Commit ca4e933

Browse files
mromaszewiczclaude
andauthored
fix: support non-indexed deepObject array unmarshaling (#22) (#96)
OpenAPI 3.0's deepObject style has undefined behavior for arrays. Two conventions exist in the wild: - Indexed: p[vals][0]=a&p[vals][1]=b (oapi-codegen's current format) - Non-indexed: p[vals]=a&p[vals]=b (Swagger UI / Rails convention) Swagger UI generates the non-indexed format, which previously failed with "[field] has multiple values". This change makes UnmarshalDeepObject accept both formats by detecting repeated query parameter keys and expanding them into synthetic indexed entries (e.g. [vals][0], [vals][1]) before feeding them into the existing tree-construction and assignment logic. Marshaling (MarshalDeepObject) intentionally remains unchanged and continues to emit the indexed format. The indexed format is unambiguous, already consumed correctly by all known implementations, and is what oapi-codegen's own generated clients expect. Changing it to the non-indexed format would be a breaking change for consumers that rely on the current wire format, with no practical benefit since both formats are now accepted on the unmarshaling side. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ead11e4 commit ca4e933

File tree

2 files changed

+44
-4
lines changed

2 files changed

+44
-4
lines changed

deepobject.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,16 @@ func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) e
136136
if strings.HasPrefix(pName, searchStr) {
137137
// trim the parameter name from the full name.
138138
pName = pName[len(paramName):]
139-
fieldNames = append(fieldNames, pName)
140-
if len(pValues) != 1 {
141-
return fmt.Errorf("%s has multiple values", pName)
139+
if len(pValues) == 1 {
140+
fieldNames = append(fieldNames, pName)
141+
fieldValues = append(fieldValues, pValues[0])
142+
} else {
143+
// Non-indexed array format: expand repeated keys into indexed entries
144+
for i, value := range pValues {
145+
fieldNames = append(fieldNames, pName+"["+strconv.Itoa(i)+"]")
146+
fieldValues = append(fieldValues, value)
147+
}
142148
}
143-
fieldValues = append(fieldValues, pValues[0])
144149
}
145150
}
146151

deepobject_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,38 @@ func TestDeepObject_ArrayOfObjects(t *testing.T) {
206206
assert.Equal(t, "second", dstArray[1].Name)
207207
assert.Equal(t, "value2", dstArray[1].Value)
208208
}
209+
210+
func TestDeepObject_NonIndexedArray(t *testing.T) {
211+
t.Run("primitive string array", func(t *testing.T) {
212+
params := url.Values{}
213+
params.Add("p[vals]", "a")
214+
params.Add("p[vals]", "b")
215+
216+
type Obj struct {
217+
Vals []string `json:"vals"`
218+
}
219+
220+
var dst Obj
221+
err := UnmarshalDeepObject(&dst, "p", params)
222+
require.NoError(t, err)
223+
assert.Equal(t, []string{"a", "b"}, dst.Vals)
224+
})
225+
226+
t.Run("object with mixed scalar and non-indexed array", func(t *testing.T) {
227+
params := url.Values{}
228+
params.Set("p[op]", "eq")
229+
params.Add("p[vals]", "a")
230+
params.Add("p[vals]", "b")
231+
232+
type Filter struct {
233+
Op string `json:"op"`
234+
Vals []string `json:"vals"`
235+
}
236+
237+
var dst Filter
238+
err := UnmarshalDeepObject(&dst, "p", params)
239+
require.NoError(t, err)
240+
assert.Equal(t, "eq", dst.Op)
241+
assert.Equal(t, []string{"a", "b"}, dst.Vals)
242+
})
243+
}

0 commit comments

Comments
 (0)