Repository: getkin/kin-openapi (go)

Issue:
deepObject parameters that are not flat cause panic
Consider the following schema:

```yaml
---
openapi: 3.0.3
info:
  title: Reproducer
  version: 0.0.1

paths:
  /example:
    get:
      operationId: Example
      parameters:
      - in: query
        name: filter
        required: false
        style: deepObject
        explode: true
        schema:
          type: object
          properties:
            labels:
              type: object
              additionalProperties:
                type: string
          required: []

      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
```

This should let one make requests with multiple levels of nesting in the filter parameter, e.g.
`/example?filter[labels][foo]=bar`

However, kin-openapi only seems to support flat key/value deepObject parameters. The example above therefore yields

```
echo: http: panic serving 127.0.0.1:58216: schema has non primitive type "object"
goroutine 46 [running]:
net/http.(*conn).serve.func1(0xc0001f1cc0)
        /usr/lib/golang/src/net/http/server.go:1800 +0x139
panic(0x124f3e0, 0xc0003280e0)
        /usr/lib/golang/src/runtime/panic.go:975 +0x3e3
github.com/getkin/kin-openapi/openapi3filter.parsePrimitive(0xc000528039, 0x24, 0xc00035f7c0, 0x6, 0xc000144868, 0xc00042f0b0, 0xc00052802c)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/req_resp_decoder.go:768 +0x86b
github.com/getkin/kin-openapi/openapi3filter.makeObject(0xc00042f0b0, 0xc00035f6e0, 0x0, 0x0, 0xc000328090)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/req_resp_decoder.go:709 +0x127
github.com/getkin/kin-openapi/openapi3filter.(*urlValuesDecoder).DecodeObject(0xc000130030, 0xc0006a1a16, 0x6, 0xc00068e100, 0xc00035f6e0, 0xc000328090, 0xc000536160, 0xc000542df0)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/req_resp_decoder.go:554 +0xd7
github.com/getkin/kin-openapi/openapi3filter.decodeValue.func2(0xc0006a1a16, 0x6, 0xc00068e100, 0xc00035f6e0, 0xc000542e10, 0x45f1e6, 0xc000130030, 0x8)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/req_resp_decoder.go:302 +0x5b
github.com/getkin/kin-openapi/openapi3filter.decodeValue(0x16eb080, 0xc000130030, 0xc0006a1a16, 0x6, 0xc00068e100, 0xc00035f6e0, 0x0, 0x1399340, 0xc000697301, 0xc00042eed0, ...)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/req_resp_decoder.go:307 +0x4bd
github.com/getkin/kin-openapi/openapi3filter.decodeStyledParameter(0xc0000a3900, 0xc00042ef00, 0xdad531, 0x203000, 0x203000, 0x203000)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/req_resp_decoder.go:235 +0x11f
github.com/getkin/kin-openapi/openapi3filter.ValidateParameter(0x16f8100, 0xc00042f050, 0xc00042ef00, 0xc0000a3900, 0xc00042f050, 0x13713c0)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/validate_request.go:140 +0x3dd
github.com/getkin/kin-openapi/openapi3filter.ValidateRequest(0x16f8100, 0xc00042f050, 0xc00042ef00, 0x16b82a0, 0x13d7600)
        /home/jharting/go/pkg/mod/github.com/getkin/kin-openapi@v0.36.0/openapi3filter/validate_request.go:65 +0x3ab
```

Pull request description:
openapi3filter: support deepObject with nested objects and array parameters
Will try to add missing nested object array items support soon, if you see no issues with the approach taken

Fixes #294 

Patch:
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index e80c050..8d9dca2 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -616,9 +616,12 @@ func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (int
 	}
 
 	return parsePrimitive(v, schema)
-
 }
 
+const (
+	urlDecoderDelimiter = "\x1F" // should not conflict with URL characters
+)
+
 func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
 	var propsFn func(url.Values) (map[string]string, error)
 	switch sm.Style {
@@ -646,12 +649,20 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
 		propsFn = func(params url.Values) (map[string]string, error) {
 			props := make(map[string]string)
 			for key, values := range params {
-				groups := regexp.MustCompile(fmt.Sprintf("%s\\[(.+?)\\]", param)).FindAllStringSubmatch(key, -1)
-				if len(groups) == 0 {
+				matches := regexp.MustCompile(`\[(.*?)\]`).FindAllStringSubmatch(key, -1)
+				switch l := len(matches); {
+				case l == 0:
 					// A query parameter's name does not match the required format, so skip it.
 					continue
+				case l == 1:
+					props[matches[0][1]] = strings.Join(values, urlDecoderDelimiter)
+				case l > 1:
+					kk := []string{}
+					for _, m := range matches {
+						kk = append(kk, m[1])
+					}
+					props[strings.Join(kk, urlDecoderDelimiter)] = strings.Join(values, urlDecoderDelimiter)
 				}
-				props[groups[0][1]] = values[0]
 			}
 			if len(props) == 0 {
 				// HTTP request does not contain query parameters encoded by rules of style "deepObject".
@@ -662,7 +673,6 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
 	default:
 		return nil, false, invalidSerializationMethodErr(sm)
 	}
-
 	props, err := propsFn(d.values)
 	if err != nil {
 		return nil, false, err
@@ -671,16 +681,30 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
 		return nil, false, nil
 	}
 
-	// check the props
+	val, err := makeObject(props, schema)
+	if err != nil {
+		return nil, false, err
+	}
+
 	found := false
 	for propName := range schema.Value.Properties {
 		if _, ok := props[propName]; ok {
 			found = true
 			break
 		}
+
+		if schema.Value.Type == "array" || schema.Value.Type == "object" {
+			for k := range props {
+				path := strings.Split(k, urlDecoderDelimiter)
+				if _, ok := deepGet(val, path...); ok {
+					found = true
+					break
+				}
+			}
+		}
 	}
-	val, err := makeObject(props, schema)
-	return val, found, err
+
+	return val, found, nil
 }
 
 // headerParamDecoder decodes values of header parameters.
@@ -845,24 +869,115 @@ func propsFromString(src, propDelim, valueDelim string) (map[string]string, erro
 	return props, nil
 }
 
+func deepGet(m map[string]interface{}, keys ...string) (interface{}, bool) {
+	for _, key := range keys {
+		val, ok := m[key]
+		if !ok {
+			return nil, false
+		}
+		if m, ok = val.(map[string]interface{}); !ok {
+			return val, true
+		}
+	}
+	return m, true
+}
+
+func deepSet(m map[string]interface{}, keys []string, value interface{}) {
+	for i := 0; i < len(keys)-1; i++ {
+		key := keys[i]
+		if _, ok := m[key]; !ok {
+			m[key] = make(map[string]interface{})
+		}
+		m = m[key].(map[string]interface{})
+	}
+	m[keys[len(keys)-1]] = value
+}
+
+func findNestedSchema(parentSchema *openapi3.SchemaRef, keys []string) (*openapi3.SchemaRef, error) {
+	currentSchema := parentSchema
+	for _, key := range keys {
+		propertySchema, ok := currentSchema.Value.Properties[key]
+		if !ok {
+			if currentSchema.Value.AdditionalProperties.Schema == nil {
+				return nil, fmt.Errorf("nested schema for key %q not found", key)
+			}
+			currentSchema = currentSchema.Value.AdditionalProperties.Schema
+			continue
+		}
+		currentSchema = propertySchema
+	}
+	return currentSchema, nil
+}
+
 // makeObject returns an object that contains properties from props.
 // A value of every property is parsed as a primitive value.
 // The function returns an error when an error happened while parse object's properties.
 func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
 	obj := make(map[string]interface{})
 	for propName, propSchema := range schema.Value.Properties {
-		value, err := parsePrimitive(props[propName], propSchema)
-		if err != nil {
-			if v, ok := err.(*ParseError); ok {
-				return nil, &ParseError{path: []interface{}{propName}, Cause: v}
+		switch propSchema.Value.Type {
+		case "array":
+			vals := strings.Split(props[propName], urlDecoderDelimiter)
+			for _, v := range vals {
+				_, err := parsePrimitive(v, propSchema.Value.Items)
+				if err != nil {
+					return nil, handlePropParseError([]string{propName}, err)
+				}
 			}
-			return nil, fmt.Errorf("property %q: %w", propName, err)
+			obj[propName] = vals
+		case "object":
+			for prop := range props {
+				if !strings.HasPrefix(prop, propName+urlDecoderDelimiter) {
+					continue
+				}
+				mapKeys := strings.Split(prop, urlDecoderDelimiter)
+				nestedSchema, err := findNestedSchema(schema, mapKeys)
+				if err != nil {
+					return nil, &ParseError{path: pathFromKeys(mapKeys), Reason: err.Error()}
+				}
+				if nestedSchema.Value.Type == "array" {
+					vals := strings.Split(props[prop], urlDecoderDelimiter)
+					for _, v := range vals {
+						_, err := parsePrimitive(v, nestedSchema.Value.Items)
+						if err != nil {
+							return nil, handlePropParseError(mapKeys, err)
+						}
+					}
+					deepSet(obj, mapKeys, vals)
+					continue
+				}
+				value, err := parsePrimitive(props[prop], nestedSchema)
+				if err != nil {
+					return nil, handlePropParseError(mapKeys, err)
+				}
+				deepSet(obj, mapKeys, value)
+			}
+		default:
+			value, err := parsePrimitive(props[propName], propSchema)
+			if err != nil {
+				return nil, handlePropParseError([]string{propName}, err)
+			}
+			obj[propName] = value
 		}
-		obj[propName] = value
 	}
 	return obj, nil
 }
 
+func handlePropParseError(path []string, err error) error {
+	if v, ok := err.(*ParseError); ok {
+		return &ParseError{path: pathFromKeys(path), Cause: v}
+	}
+	return fmt.Errorf("property %q: %w", strings.Join(path, "."), err)
+}
+
+func pathFromKeys(kk []string) []interface{} {
+	path := make([]interface{}, len(kk))
+	for i, v := range kk {
+		path[i] = v
+	}
+	return path
+}
+
 // parseArray returns an array that contains items from a raw array.
 // Every item is parsed as a primitive value.
 // The function returns an error when an error happened while parse array's items.
@@ -923,7 +1038,7 @@ func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error)
 	case "string":
 		return raw, nil
 	default:
-		panic(fmt.Sprintf("schema has non primitive type %q", schema.Value.Type))
+		return nil, &ParseError{Kind: KindOther, Value: raw, Reason: "schema has non primitive type " + schema.Value.Type}
 	}
 }
 
@@ -1180,10 +1295,10 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
 				if anyProperties := schema.Value.AdditionalProperties.Has; anyProperties != nil {
 					switch *anyProperties {
 					case true:
-						//additionalProperties: true
+						// additionalProperties: true
 						continue
 					default:
-						//additionalProperties: false
+						// additionalProperties: false
 						return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
 					}
 				}
@@ -1300,7 +1415,6 @@ func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch
 
 			return nil
 		}()
-
 		if err != nil {
 			return nil, err
 		}


Test patch:
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 967f1ba..334af84 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -40,30 +40,39 @@ func TestDecodeParameter(t *testing.T) {
 			return s
 		}
 
-		integerSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "integer"}}
-		numberSchema  = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "number"}}
-		booleanSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "boolean"}}
-		stringSchema  = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}}
-		allofSchema   = &openapi3.SchemaRef{
+		integerSchema                          = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "integer"}}
+		numberSchema                           = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "number"}}
+		booleanSchema                          = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "boolean"}}
+		stringSchema                           = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}}
+		additionalPropertiesObjectStringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", AdditionalProperties: openapi3.AdditionalProperties{Schema: stringSchema}}}
+		additionalPropertiesObjectBoolSchema   = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", AdditionalProperties: openapi3.AdditionalProperties{Schema: booleanSchema}}}
+		allofSchema                            = &openapi3.SchemaRef{
 			Value: &openapi3.Schema{
 				AllOf: []*openapi3.SchemaRef{
 					integerSchema,
 					numberSchema,
-				}}}
+				},
+			},
+		}
 		anyofSchema = &openapi3.SchemaRef{
 			Value: &openapi3.Schema{
 				AnyOf: []*openapi3.SchemaRef{
 					integerSchema,
 					stringSchema,
-				}}}
+				},
+			},
+		}
 		oneofSchema = &openapi3.SchemaRef{
 			Value: &openapi3.Schema{
 				OneOf: []*openapi3.SchemaRef{
 					booleanSchema,
 					integerSchema,
-				}}}
-		arraySchema  = arrayOf(stringSchema)
-		objectSchema = objectOf("id", stringSchema, "name", stringSchema)
+				},
+			},
+		}
+		stringArraySchema  = arrayOf(stringSchema)
+		integerArraySchema = arrayOf(integerSchema)
+		objectSchema       = objectOf("id", stringSchema, "name", stringSchema)
 	)
 
 	type testCase struct {
@@ -218,77 +227,77 @@ func TestDecodeParameter(t *testing.T) {
 			testCases: []testCase{
 				{
 					name:  "simple",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: stringArraySchema},
 					path:  "/foo,bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "simple explode",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: stringArraySchema},
 					path:  "/foo,bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "label",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringArraySchema},
 					path:  "/.foo,bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "label invalid",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringArraySchema},
 					path:  "/foo,bar",
 					found: true,
 					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
 				},
 				{
 					name:  "label explode",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringArraySchema},
 					path:  "/.foo.bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "label explode invalid",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringArraySchema},
 					path:  "/foo.bar",
 					found: true,
 					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo.bar"},
 				},
 				{
 					name:  "matrix",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringArraySchema},
 					path:  "/;param=foo,bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "matrix invalid",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringArraySchema},
 					path:  "/foo,bar",
 					found: true,
 					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
 				},
 				{
 					name:  "matrix explode",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringArraySchema},
 					path:  "/;param=foo;param=bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "matrix explode invalid",
-					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringArraySchema},
 					path:  "/foo,bar",
 					found: true,
 					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
 				},
 				{
 					name:  "default",
-					param: &openapi3.Parameter{Name: "param", In: "path", Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringArraySchema},
 					path:  "/foo,bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
@@ -563,49 +572,49 @@ func TestDecodeParameter(t *testing.T) {
 			testCases: []testCase{
 				{
 					name:  "form",
-					param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: stringArraySchema},
 					query: "param=foo,bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "form explode",
-					param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: stringArraySchema},
 					query: "param=foo&param=bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "spaceDelimited",
-					param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: stringArraySchema},
 					query: "param=foo bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "spaceDelimited explode",
-					param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: stringArraySchema},
 					query: "param=foo&param=bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "pipeDelimited",
-					param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: stringArraySchema},
 					query: "param=foo|bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "pipeDelimited explode",
-					param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: stringArraySchema},
 					query: "param=foo&param=bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
 				},
 				{
 					name:  "default",
-					param: &openapi3.Parameter{Name: "param", In: "query", Schema: arraySchema},
+					param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringArraySchema},
 					query: "param=foo&param=bar",
 					want:  []interface{}{"foo", "bar"},
 					found: true,
@@ -657,6 +666,194 @@ func TestDecodeParameter(t *testing.T) {
 					want:  map[string]interface{}{"id": "foo", "name": "bar"},
 					found: true,
 				},
+				{
+					name:  "deepObject explode array",
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectOf("items", stringArraySchema)},
+					query: "param[items]=f%26oo&param[items]=bar",
+					want:  map[string]interface{}{"items": []string{"f%26oo", "bar"}},
+					found: true,
+				},
+				{
+					name: "deepObject explode nested object additionalProperties",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", additionalPropertiesObjectStringSchema,
+							"objTwo", stringSchema,
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][prop1]=bar&param[obj][prop2]=foo&param[objTwo]=string",
+					want: map[string]interface{}{
+						"obj":    map[string]interface{}{"prop1": "bar", "prop2": "foo"},
+						"objTwo": "string",
+					},
+					found: true,
+				},
+				{
+					name: "deepObject explode nested object additionalProperties - bad value",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", additionalPropertiesObjectBoolSchema,
+							"objTwo", stringSchema,
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][prop1]=notbool&param[objTwo]=string",
+					err:   &ParseError{path: []interface{}{"obj", "prop1"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "notbool"}},
+				},
+				{
+					name: "deepObject explode nested object additionalProperties - bad index inside additionalProperties",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", additionalPropertiesObjectStringSchema,
+							"objTwo", stringSchema,
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][prop1]=bar&param[obj][prop2][badindex]=bad&param[objTwo]=string",
+					err:   &ParseError{path: []interface{}{"obj", "prop2", "badindex"}, Reason: `nested schema for key "badindex" not found`},
+				},
+				{
+					name: "deepObject explode nested object",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema),
+							"objTwo", stringSchema,
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo]=string",
+					want: map[string]interface{}{
+						"obj":    map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"},
+						"objTwo": "string",
+					},
+					found: true,
+				},
+				{
+					name: "deepObject explode nested object - bad index",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", stringSchema),
+						),
+					},
+					query: "param[obj][badindex]=bar",
+					found: true,
+					err:   &ParseError{path: []interface{}{"obj", "badindex"}, Reason: `nested schema for key "badindex" not found`},
+				},
+				{
+					name: "deepObject explode nested object - extraneous param ignored",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema),
+						),
+					},
+					query: "param[unknown]=bar",
+					want:  map[string]interface{}{},
+				},
+				{
+					name: "deepObject explode nested object - bad array item type",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"objTwo", integerArraySchema,
+						),
+					},
+					query: "param[objTwo]=badint",
+					err:   &ParseError{path: []interface{}{"objTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}},
+				},
+				{
+					name: "deepObject explode deeply nested object - bad array item type",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", integerArraySchema),
+						),
+					},
+					query: "param[obj][nestedObjOne]=badint",
+					err:   &ParseError{path: []interface{}{"obj", "nestedObjOne"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}},
+				},
+				{
+					name: "deepObject explode nested object with array",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema),
+							"objTwo", stringArraySchema,
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo]=f%26oo&param[objTwo]=bar",
+					want: map[string]interface{}{
+						"obj":    map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"},
+						"objTwo": []string{"f%26oo", "bar"},
+					},
+					found: true,
+				},
+				{
+					name: "deepObject explode nested object with array - bad value",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", booleanSchema),
+							"objTwo", stringArraySchema,
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=bad&param[objTwo]=f%26oo&param[objTwo]=bar",
+					err:   &ParseError{path: []interface{}{"obj", "nestedObjTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bad"}},
+				},
+				{
+					name: "deepObject explode nested object with nested array",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema),
+							"objTwo", objectOf("items", stringArraySchema),
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo][items]=f%26oo&param[objTwo][items]=bar",
+					want: map[string]interface{}{
+						"obj":    map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"},
+						"objTwo": map[string]interface{}{"items": []string{"f%26oo", "bar"}},
+					},
+					found: true,
+				},
+
+				{
+					name: "deepObject explode nested object with nested array on different levels",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", objectOf("items", stringArraySchema)),
+							"objTwo", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][nestedObjOne][items]=baz&param[objTwo][items]=foo&param[objTwo][items]=bar",
+					want: map[string]interface{}{
+						"obj":    map[string]interface{}{"nestedObjOne": map[string]interface{}{"items": []string{"baz"}}},
+						"objTwo": map[string]interface{}{"items": []string{"foo", "bar"}},
+					},
+					found: true,
+				},
+				{
+					name: "deepObject explode nested object array bad index",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", objectOf("nestedObjOne", objectOf("items", stringArraySchema)),
+						),
+					},
+					query: "param[obj][nestedObjOne]=baz",
+					found: true,
+					err:   &ParseError{path: []interface{}{"obj", "nestedObjOne"}, Cause: &ParseError{Kind: KindOther, Value: "baz", Reason: "schema has non primitive type object"}},
+				},
 				{
 					name:  "default",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectSchema},
@@ -767,21 +964,21 @@ func TestDecodeParameter(t *testing.T) {
 			testCases: []testCase{
 				{
 					name:   "simple",
-					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: arraySchema},
+					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: stringArraySchema},
 					header: "X-Param:foo,bar",
 					want:   []interface{}{"foo", "bar"},
 					found:  true,
 				},
 				{
 					name:   "simple explode",
-					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: arraySchema},
+					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: stringArraySchema},
 					header: "X-Param:foo,bar",
 					want:   []interface{}{"foo", "bar"},
 					found:  true,
 				},
 				{
 					name:   "default",
-					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arraySchema},
+					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringArraySchema},
 					header: "X-Param:foo,bar",
 					want:   []interface{}{"foo", "bar"},
 					found:  true,
@@ -943,7 +1140,7 @@ func TestDecodeParameter(t *testing.T) {
 			testCases: []testCase{
 				{
 					name:   "form",
-					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arraySchema},
+					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: stringArraySchema},
 					cookie: "X-Param:foo,bar",
 					want:   []interface{}{"foo", "bar"},
 					found:  true,
@@ -1060,16 +1257,17 @@ func TestDecodeParameter(t *testing.T) {
 					input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route}
 					got, found, err := decodeStyledParameter(tc.param, input)
 
-					require.Truef(t, found == tc.found, "got found: %t, want found: %t", found, tc.found)
-
 					if tc.err != nil {
 						require.Error(t, err)
 						require.Truef(t, matchParseError(err, tc.err), "got error:\n%v\nwant error:\n%v", err, tc.err)
+
 						return
 					}
 
 					require.NoError(t, err)
-					require.Truef(t, reflect.DeepEqual(got, tc.want), "got %v, want %v", got, tc.want)
+					require.EqualValues(t, tc.want, got)
+
+					require.Truef(t, found == tc.found, "got found: %t, want found: %t", found, tc.found)
 				})
 			}
 		})