Repository: getkin/kin-openapi
Pull request #923 (2024-03-12 15:56:01)

openapi3filter: deepObject array of objects and array of arrays support

danicc097: I don't see any changes? @fenollp 

Patch:
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index 2861349..4bcc3ae 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -469,6 +469,7 @@ func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.Serialization
 	if err != nil {
 		return nil, ok, err
 	}
+
 	val, err := makeObject(props, schema)
 	return val, ok, err
 }
@@ -654,9 +655,7 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
 				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:
+				case l >= 1:
 					kk := []string{}
 					for _, m := range matches {
 						kk = append(kk, m[1])
@@ -680,7 +679,6 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
 	if props == nil {
 		return nil, false, nil
 	}
-
 	val, err := makeObject(props, schema)
 	if err != nil {
 		return nil, false, err
@@ -896,127 +894,198 @@ func deepSet(m map[string]interface{}, keys []string, value interface{}) {
 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)
+		if currentSchema.Value.Type.Includes(openapi3.TypeArray) {
+			currentSchema = currentSchema.Value.Items
+		} else {
+			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 = currentSchema.Value.AdditionalProperties.Schema
-			continue
+			currentSchema = propertySchema
 		}
-		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 {
-		switch {
-		case propSchema.Value.Type.Is("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)
-				}
-			}
+	mobj := make(map[string]interface{})
 
-			ivals, err := convertArrayParameterToType(vals, propSchema.Value.Items.Value.Type)
-			if err != nil {
-				return nil, handlePropParseError([]string{propName}, err)
-			}
-			obj[propName] = ivals
-		case propSchema.Value.Type.Is("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.Permits("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)
-						}
-					}
-					ivals, err := convertArrayParameterToType(vals, nestedSchema.Value.Items.Value.Type)
-					if err != nil {
-						return nil, handlePropParseError(mapKeys, err)
-					}
-					deepSet(obj, mapKeys, ivals)
-					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
+	for kk, value := range props {
+		keys := strings.Split(kk, urlDecoderDelimiter)
+		if strings.Contains(value, urlDecoderDelimiter) {
+			// don't support implicit array indexes anymore
+			p := pathFromKeys(keys)
+			return nil, &ParseError{path: p, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}
 		}
+		deepSet(mobj, keys, value)
+	}
+	r, err := buildResObj(mobj, nil, "", schema)
+	if err != nil {
+		return nil, err
+	}
+	result, ok := r.(map[string]interface{})
+	if !ok {
+		return nil, &ParseError{Kind: KindOther, Reason: "invalid param object", Value: result}
 	}
 
-	return obj, nil
+	return result, nil
 }
 
-func convertArrayParameterToType(strArray []string, typ *openapi3.Types) (interface{}, error) {
-	var iarr []interface{}
+// example: map[0:map[key:true] 1:map[key:false]] -> [map[key:true] map[key:false]]
+func sliceMapToSlice(m map[string]interface{}) ([]interface{}, error) {
+	var result []interface{}
+
+	keys := make([]int, 0, len(m))
+	for k := range m {
+		key, err := strconv.Atoi(k)
+		if err != nil {
+			return nil, fmt.Errorf("array indexes must be integers: %w", err)
+		}
+		keys = append(keys, key)
+	}
+	max := -1
+	for _, k := range keys {
+		if k > max {
+			max = k
+		}
+	}
+	for i := 0; i <= max; i++ {
+		val, ok := m[strconv.Itoa(i)]
+		if !ok {
+			result = append(result, nil)
+			continue
+		}
+		result = append(result, val)
+	}
+	return result, nil
+}
+
+// buildResObj constructs an object based on a given schema and param values
+func buildResObj(params map[string]interface{}, parentKeys []string, key string, schema *openapi3.SchemaRef) (interface{}, error) {
+	mapKeys := parentKeys
+	if key != "" {
+		mapKeys = append(mapKeys, key)
+	}
+
 	switch {
-	case typ.Permits(openapi3.TypeBoolean):
-		for _, str := range strArray {
-			if str == "" {
-				continue
-			}
-			parsedBool, err := strconv.ParseBool(str)
+	case schema.Value.Type.Is("array"):
+		paramArr, ok := deepGet(params, mapKeys...)
+		if !ok {
+			return nil, nil
+		}
+		t, isMap := paramArr.(map[string]interface{})
+		if !isMap {
+			return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}
+		}
+		// intermediate arrays have to be instantiated
+		arr, err := sliceMapToSlice(t)
+		if err != nil {
+			return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: fmt.Sprintf("could not convert value map to array: %v", err)}
+		}
+		resultArr := make([]interface{}, len(arr))
+		for i := range arr {
+			r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items)
 			if err != nil {
 				return nil, err
 			}
-			iarr = append(iarr, parsedBool)
-		}
-	case typ.Permits(openapi3.TypeInteger):
-		for _, str := range strArray {
-			if str == "" {
-				continue
+			if r != nil {
+				resultArr[i] = r
 			}
-			parsedInt, err := strconv.Atoi(str)
+		}
+		return resultArr, nil
+	case schema.Value.Type.Is("object"):
+		resultMap := make(map[string]interface{})
+		additPropsSchema := schema.Value.AdditionalProperties.Schema
+		pp, _ := deepGet(params, mapKeys...)
+		objectParams, ok := pp.(map[string]interface{})
+		if !ok {
+			// not the expected type, but return it either way and leave validation up to ValidateParameter
+			return pp, nil
+		}
+		for k, propSchema := range schema.Value.Properties {
+			r, err := buildResObj(params, mapKeys, k, propSchema)
 			if err != nil {
 				return nil, err
 			}
-			iarr = append(iarr, parsedInt)
+			if r != nil {
+				resultMap[k] = r
+			}
+		}
+		if additPropsSchema != nil {
+			// dynamic creation of possibly nested objects
+			for k := range objectParams {
+				r, err := buildResObj(params, mapKeys, k, additPropsSchema)
+				if err != nil {
+					return nil, err
+				}
+				if r != nil {
+					resultMap[k] = r
+				}
+			}
+		}
+
+		return resultMap, nil
+	case len(schema.Value.AnyOf) > 0:
+		return buildFromSchemas(schema.Value.AnyOf, params, parentKeys, key)
+	case len(schema.Value.OneOf) > 0:
+		return buildFromSchemas(schema.Value.OneOf, params, parentKeys, key)
+	case len(schema.Value.AllOf) > 0:
+		return buildFromSchemas(schema.Value.AllOf, params, parentKeys, key)
+	default:
+		val, ok := deepGet(params, mapKeys...)
+		if !ok {
+			// leave validation up to ValidateParameter. here there really is not parameter set
+			return nil, nil
+		}
+		v, ok := val.(string)
+		if !ok {
+			return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Value: val, Reason: "path is not convertible to primitive"}
 		}
-	case typ.Permits(openapi3.TypeNumber):
-		for _, str := range strArray {
-			if str == "" {
+		prim, err := parsePrimitive(v, schema)
+		if err != nil {
+			return nil, handlePropParseError(mapKeys, err)
+		}
+
+		return prim, nil
+	}
+}
+
+// buildFromSchemas decodes params with anyOf, oneOf, allOf schemas.
+func buildFromSchemas(schemas openapi3.SchemaRefs, params map[string]interface{}, mapKeys []string, key string) (interface{}, error) {
+	resultMap := make(map[string]interface{})
+	for _, s := range schemas {
+		val, err := buildResObj(params, mapKeys, key, s)
+		if err == nil && val != nil {
+
+			if m, ok := val.(map[string]interface{}); ok {
+				for k, v := range m {
+					resultMap[k] = v
+				}
 				continue
 			}
-			parsedFloat, err := strconv.ParseFloat(str, 64)
-			if err != nil {
-				return nil, err
+
+			if a, ok := val.([]interface{}); ok {
+				if len(a) > 0 {
+					return a, nil
+				}
+				continue
 			}
-			iarr = append(iarr, parsedFloat)
+
+			// if its a primitive and not nil just return that and let it be validated
+			return val, nil
 		}
-	case typ.Permits(openapi3.TypeString):
-		return strArray, nil
-	default:
-		return nil, fmt.Errorf("unsupported parameter array type: %s", typ)
 	}
 
-	return iarr, nil
+	if len(resultMap) > 0 {
+		return resultMap, nil
+	}
+
+	return nil, nil
 }
 
 func handlePropParseError(path []string, err error) error {


Test patch:
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 7e5f813..f24b245 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -14,67 +14,279 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/getkin/kin-openapi/openapi3"
 	legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
 )
 
-func TestDecodeParameter(t *testing.T) {
-	var (
-		explode   = openapi3.BoolPtr(true)
-		noExplode = openapi3.BoolPtr(false)
-		arrayOf   = func(items *openapi3.SchemaRef) *openapi3.SchemaRef {
-			return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"array"}, Items: items}}
+var (
+	explode   = openapi3.BoolPtr(true)
+	noExplode = openapi3.BoolPtr(false)
+	arrayOf   = func(items *openapi3.SchemaRef) *openapi3.SchemaRef {
+		return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"array"}, Items: items}}
+	}
+	objectOf = func(args ...interface{}) *openapi3.SchemaRef {
+		s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, Properties: make(map[string]*openapi3.SchemaRef)}}
+		if len(args)%2 != 0 {
+			panic("invalid arguments. must be an even number of arguments")
 		}
-		objectOf = func(args ...interface{}) *openapi3.SchemaRef {
-			s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, Properties: make(map[string]*openapi3.SchemaRef)}}
-			if len(args)%2 != 0 {
-				panic("invalid arguments. must be an even number of arguments")
-			}
-			for i := 0; i < len(args)/2; i++ {
-				propName := args[i*2].(string)
-				propSchema := args[i*2+1].(*openapi3.SchemaRef)
-				s.Value.Properties[propName] = propSchema
-			}
-			return s
+		for i := 0; i < len(args)/2; i++ {
+			propName := args[i*2].(string)
+			propSchema := args[i*2+1].(*openapi3.SchemaRef)
+			s.Value.Properties[propName] = propSchema
 		}
+		return s
+	}
+
+	additionalPropertiesObjectOf = func(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
+		return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{Schema: schema}}}
+	}
+
+	integerSchema                          = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}}
+	numberSchema                           = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"number"}}}
+	booleanSchema                          = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"boolean"}}}
+	stringSchema                           = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}
+	additionalPropertiesObjectStringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{Schema: stringSchema}}}
+	additionalPropertiesObjectBoolSchema   = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"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,
+			},
+		},
+	}
+	oneofSchemaObject = &openapi3.SchemaRef{
+		Value: &openapi3.Schema{
+			OneOf: []*openapi3.SchemaRef{
+				objectOneRSchema,
+				objectTwoRSchema,
+			},
+		},
+	}
+	anyofSchemaObject = &openapi3.SchemaRef{
+		Value: &openapi3.Schema{
+			AnyOf: []*openapi3.SchemaRef{
+				objectOneRSchema,
+				objectTwoRSchema,
+			},
+		},
+	}
+	stringArraySchema  = arrayOf(stringSchema)
+	integerArraySchema = arrayOf(integerSchema)
+	objectSchema       = objectOf("id", stringSchema, "name", stringSchema)
+	objectTwoRSchema   = func() *openapi3.SchemaRef {
+		s := objectOf("id2", stringSchema, "name2", stringSchema)
+		s.Value.Required = []string{"id2"}
+
+		return s
+	}()
+
+	objectOneRSchema = func() *openapi3.SchemaRef {
+		s := objectOf("id", stringSchema, "name", stringSchema)
+		s.Value.Required = []string{"id"}
+
+		return s
+	}()
+
+	oneofSchemaArrayObject = &openapi3.SchemaRef{
+		Value: &openapi3.Schema{
+			AnyOf: []*openapi3.SchemaRef{
+				stringArraySchema,
+				objectTwoRSchema,
+			},
+		},
+	}
+)
+
+func TestDeepGet(t *testing.T) {
+	iarray := map[string]interface{}{
+		"0": map[string]interface{}{
+			"foo": 111,
+		},
+		"1": map[string]interface{}{
+			"bar": 222,
+		},
+	}
 
-		integerSchema                          = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}}
-		numberSchema                           = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"number"}}}
-		booleanSchema                          = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"boolean"}}}
-		stringSchema                           = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}
-		additionalPropertiesObjectStringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{Schema: stringSchema}}}
-		additionalPropertiesObjectBoolSchema   = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{Schema: booleanSchema}}}
-		allofSchema                            = &openapi3.SchemaRef{
-			Value: &openapi3.Schema{
-				AllOf: []*openapi3.SchemaRef{
-					integerSchema,
-					numberSchema,
+	tests := []struct {
+		name       string
+		m          map[string]interface{}
+		keys       []string
+		expected   interface{}
+		shouldFind bool
+	}{
+		{
+			name: "Simple map - key exists",
+			m: map[string]interface{}{
+				"foo": "bar",
+			},
+			keys:       []string{"foo"},
+			expected:   "bar",
+			shouldFind: true,
+		},
+		{
+			name: "Nested map - key exists",
+			m: map[string]interface{}{
+				"foo": map[string]interface{}{
+					"bar": "baz",
 				},
 			},
-		}
-		anyofSchema = &openapi3.SchemaRef{
-			Value: &openapi3.Schema{
-				AnyOf: []*openapi3.SchemaRef{
-					integerSchema,
-					stringSchema,
+			keys:       []string{"foo", "bar"},
+			expected:   "baz",
+			shouldFind: true,
+		},
+		{
+			name: "Nested map - key does not exist",
+			m: map[string]interface{}{
+				"foo": map[string]interface{}{
+					"bar": "baz",
 				},
 			},
-		}
-		oneofSchema = &openapi3.SchemaRef{
-			Value: &openapi3.Schema{
-				OneOf: []*openapi3.SchemaRef{
-					booleanSchema,
-					integerSchema,
+			keys:       []string{"foo", "baz"},
+			expected:   nil,
+			shouldFind: false,
+		},
+		{
+			name: "Array - element exists",
+			m: map[string]interface{}{
+				"array": map[string]interface{}{"0": "a", "1": "b", "2": "c"},
+			},
+			keys:       []string{"array", "1"},
+			expected:   "b",
+			shouldFind: true,
+		},
+		{
+			name: "Array - element does not exist - invalid index",
+			m: map[string]interface{}{
+				"array": map[string]interface{}{"0": "a", "1": "b", "2": "c"},
+			},
+			keys:       []string{"array", "3"},
+			expected:   nil,
+			shouldFind: false,
+		},
+		{
+			name: "Array - element does not exist - invalid keys",
+			m: map[string]interface{}{
+				"array": map[string]interface{}{"0": "a", "1": "b", "2": "c"},
+			},
+			keys:       []string{"array", "a", "999"},
+			expected:   nil,
+			shouldFind: false,
+		},
+		{
+			name: "Array of objects - element exists 1",
+			m: map[string]interface{}{
+				"array": iarray,
+			},
+			keys:       []string{"array", "1", "bar"},
+			expected:   222,
+			shouldFind: true,
+		},
+		{
+			name: "Array of objects - element exists 2",
+			m: map[string]interface{}{
+				"array": iarray,
+			},
+			keys: []string{"array", "0"},
+			expected: map[string]interface{}{
+				"foo": 111,
+			},
+			shouldFind: true,
+		},
+		{
+			name: "Array of objects - element exists 3",
+			m: map[string]interface{}{
+				"array": iarray,
+			},
+			keys:       []string{"array"},
+			expected:   iarray,
+			shouldFind: true,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			tc := tc
+
+			result, found := deepGet(tc.m, tc.keys...)
+			require.Equal(t, tc.shouldFind, found, "shouldFind mismatch")
+			require.Equal(t, tc.expected, result, "result mismatch")
+		})
+	}
+}
+
+func TestDeepSet(t *testing.T) {
+	tests := []struct {
+		name     string
+		inputMap map[string]interface{}
+		keys     []string
+		value    interface{}
+		expected map[string]interface{}
+	}{
+		{
+			name:     "simple set",
+			inputMap: map[string]interface{}{},
+			keys:     []string{"key"},
+			value:    "value",
+			expected: map[string]interface{}{"key": "value"},
+		},
+		{
+			name:     "intermediate array of objects",
+			inputMap: map[string]interface{}{},
+			keys:     []string{"nested", "0", "key"},
+			value:    true,
+			expected: map[string]interface{}{
+				"nested": map[string]interface{}{
+					"0": map[string]interface{}{
+						"key": true,
+					},
 				},
 			},
-		}
-		stringArraySchema  = arrayOf(stringSchema)
-		integerArraySchema = arrayOf(integerSchema)
-		objectSchema       = objectOf("id", stringSchema, "name", stringSchema)
-	)
+		},
+		{
+			name:     "existing nested array of objects",
+			inputMap: map[string]interface{}{"nested": map[string]interface{}{"0": map[string]interface{}{"existingKey": "existingValue"}}},
+			keys:     []string{"nested", "0", "newKey"},
+			value:    "newValue",
+			expected: map[string]interface{}{
+				"nested": map[string]interface{}{
+					"0": map[string]interface{}{
+						"existingKey": "existingValue",
+						"newKey":      "newValue",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			deepSet(tc.inputMap, tc.keys, tc.value)
+			require.EqualValues(t, tc.expected, tc.inputMap)
+		})
+	}
+}
 
+func TestDecodeParameter(t *testing.T) {
 	type testCase struct {
 		name   string
 		param  *openapi3.Parameter
@@ -667,10 +879,29 @@ func TestDecodeParameter(t *testing.T) {
 					found: true,
 				},
 				{
-					name:  "deepObject explode array",
+					name: "deepObject explode additionalProperties with object properties - missing index on nested array",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", additionalPropertiesObjectOf(objectOf("item1", integerSchema, "item2", stringArraySchema)),
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][prop2][item2]=def",
+					err:   &ParseError{path: []interface{}{"obj", "prop2", "item2"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"},
+				},
+				{
+					name:  "deepObject explode array - missing indexes",
 					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,
+					err:   &ParseError{path: []interface{}{"items"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"},
+				},
+				{
+					name:  "deepObject explode array",
+					param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectOf("items", integerArraySchema)},
+					query: "param[items][1]=456&param[items][0]=123",
+					want:  map[string]interface{}{"items": []interface{}{int64(123), int64(456)}},
 					found: true,
 				},
 				{
@@ -690,6 +921,24 @@ func TestDecodeParameter(t *testing.T) {
 					},
 					found: true,
 				},
+				{
+					name: "deepObject explode additionalProperties with object properties - sharing property",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"obj", additionalPropertiesObjectOf(objectOf("item1", integerSchema, "item2", stringSchema)),
+							"objIgnored", objectOf("items", stringArraySchema),
+						),
+					},
+					query: "param[obj][prop1][item1]=1&param[obj][prop1][item2]=abc",
+					want: map[string]interface{}{
+						"obj": map[string]interface{}{"prop1": map[string]interface{}{
+							"item1": int64(1),
+							"item2": "abc",
+						}},
+					},
+					found: true,
+				},
 				{
 					name: "deepObject explode nested object additionalProperties - bad value",
 					param: &openapi3.Parameter{
@@ -714,7 +963,12 @@ func TestDecodeParameter(t *testing.T) {
 						),
 					},
 					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`},
+					err: &ParseError{
+						path:   []interface{}{"obj", "prop2"},
+						Reason: `path is not convertible to primitive`,
+						Kind:   KindInvalidFormat,
+						Value:  map[string]interface{}{"badindex": "bad"},
+					},
 				},
 				{
 					name: "deepObject explode nested object",
@@ -734,49 +988,48 @@ func TestDecodeParameter(t *testing.T) {
 					found: true,
 				},
 				{
-					name: "deepObject explode nested object - bad index",
+					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),
+							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema),
 						),
 					},
-					query: "param[obj][badindex]=bar",
-					found: true,
-					err:   &ParseError{path: []interface{}{"obj", "badindex"}, Reason: `nested schema for key "badindex" not found`},
+					query: "anotherparam=bar",
+					want:  map[string]interface{}(nil),
 				},
 				{
-					name: "deepObject explode nested object - extraneous param ignored",
+					name: "deepObject explode nested object - bad array item type",
 					param: &openapi3.Parameter{
 						Name: "param", In: "query", Style: "deepObject", Explode: explode,
 						Schema: objectOf(
-							"obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema),
+							"objTwo", integerArraySchema,
 						),
 					},
-					query: "param[unknown]=bar",
-					want:  map[string]interface{}{},
+					query: "param[objTwo][0]=badint",
+					err:   &ParseError{path: []interface{}{"objTwo", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}},
 				},
 				{
-					name: "deepObject explode nested object - bad array item type",
+					name: "deepObject explode deeply nested object - bad array item type",
 					param: &openapi3.Parameter{
 						Name: "param", In: "query", Style: "deepObject", Explode: explode,
 						Schema: objectOf(
-							"objTwo", integerArraySchema,
+							"obj", objectOf("nestedObjOne", integerArraySchema),
 						),
 					},
-					query: "param[objTwo]=badint",
-					err:   &ParseError{path: []interface{}{"objTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}},
+					query: "param[obj][nestedObjOne][0]=badint",
+					err:   &ParseError{path: []interface{}{"obj", "nestedObjOne", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}},
 				},
 				{
-					name: "deepObject explode deeply nested object - bad array item type",
+					name: "deepObject explode deeply nested object - array index not an int",
 					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"}},
+					query: "param[obj][nestedObjOne][badindex]=badint",
+					err:   &ParseError{path: []interface{}{"obj", "nestedObjOne"}, Kind: KindInvalidFormat, Reason: "could not convert value map to array: array indexes must be integers: strconv.Atoi: parsing \"badindex\": invalid syntax"},
 				},
 				{
 					name: "deepObject explode nested object with array",
@@ -788,10 +1041,10 @@ func TestDecodeParameter(t *testing.T) {
 							"objIgnored", objectOf("items", stringArraySchema),
 						),
 					},
-					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo]=f%26oo&param[objTwo]=bar",
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo][0]=f%26oo&param[objTwo][1]=bar",
 					want: map[string]interface{}{
 						"obj":    map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"},
-						"objTwo": []string{"f%26oo", "bar"},
+						"objTwo": []interface{}{"f%26oo", "bar"},
 					},
 					found: true,
 				},
@@ -805,7 +1058,7 @@ func TestDecodeParameter(t *testing.T) {
 							"objIgnored", objectOf("items", stringArraySchema),
 						),
 					},
-					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=bad&param[objTwo]=f%26oo&param[objTwo]=bar",
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=bad&param[objTwo][0]=f%26oo&param[objTwo][1]=bar",
 					err:   &ParseError{path: []interface{}{"obj", "nestedObjTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bad"}},
 				},
 				{
@@ -818,14 +1071,13 @@ func TestDecodeParameter(t *testing.T) {
 							"objIgnored", objectOf("items", stringArraySchema),
 						),
 					},
-					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo][items]=f%26oo&param[objTwo][items]=bar",
+					query: "param[obj][nestedObjOne]=bar&param[obj][nestedObjTwo]=foo&param[objTwo][items][0]=f%26oo&param[objTwo][items][1]=bar",
 					want: map[string]interface{}{
 						"obj":    map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"},
-						"objTwo": map[string]interface{}{"items": []string{"f%26oo", "bar"}},
+						"objTwo": map[string]interface{}{"items": []interface{}{"f%26oo", "bar"}},
 					},
 					found: true,
 				},
-
 				{
 					name: "deepObject explode nested object with nested array on different levels",
 					param: &openapi3.Parameter{
@@ -835,24 +1087,65 @@ func TestDecodeParameter(t *testing.T) {
 							"objTwo", objectOf("items", stringArraySchema),
 						),
 					},
-					query: "param[obj][nestedObjOne][items]=baz&param[objTwo][items]=foo&param[objTwo][items]=bar",
+					query: "param[obj][nestedObjOne][items][0]=baz&param[objTwo][items][0]=foo&param[objTwo][items][1]=bar",
 					want: map[string]interface{}{
-						"obj":    map[string]interface{}{"nestedObjOne": map[string]interface{}{"items": []string{"baz"}}},
-						"objTwo": map[string]interface{}{"items": []string{"foo", "bar"}},
+						"obj":    map[string]interface{}{"nestedObjOne": map[string]interface{}{"items": []interface{}{"baz"}}},
+						"objTwo": map[string]interface{}{"items": []interface{}{"foo", "bar"}},
 					},
 					found: true,
 				},
 				{
-					name: "deepObject explode nested object array bad index",
+					name: "deepObject explode array of arrays",
 					param: &openapi3.Parameter{
 						Name: "param", In: "query", Style: "deepObject", Explode: explode,
 						Schema: objectOf(
-							"obj", objectOf("nestedObjOne", objectOf("items", stringArraySchema)),
+							"arr", arrayOf(arrayOf(integerSchema)),
 						),
 					},
-					query: "param[obj][nestedObjOne]=baz",
+					query: "param[arr][1][1]=123&param[arr][1][2]=456",
+					want: map[string]interface{}{
+						"arr": []interface{}{
+							nil,
+							[]interface{}{nil, int64(123), int64(456)},
+						},
+					},
 					found: true,
-					err:   &ParseError{path: []interface{}{"obj", "nestedObjOne"}, Cause: &ParseError{Kind: KindOther, Value: "baz", Reason: "schema has non primitive type object"}},
+				},
+				{
+					name: "deepObject explode nested array of objects - missing intermediate array index",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"arr", arrayOf(objectOf("key", booleanSchema)),
+						),
+					},
+					query: "param[arr][3][key]=true&param[arr][0][key]=false",
+					want: map[string]interface{}{
+						"arr": []interface{}{
+							map[string]interface{}{"key": false},
+							nil,
+							nil,
+							map[string]interface{}{"key": true},
+						},
+					},
+					found: true,
+				},
+				{
+					name: "deepObject explode nested array of objects",
+					param: &openapi3.Parameter{
+						Name: "param", In: "query", Style: "deepObject", Explode: explode,
+						Schema: objectOf(
+							"arr", arrayOf(objectOf("key", booleanSchema)),
+						),
+					},
+					query: "param[arr][0][key]=true&param[arr][1][key]=false",
+					found: true,
+					want: map[string]interface{}{
+						"arr": []interface{}{
+							map[string]interface{}{"key": true},
+							map[string]interface{}{"key": false},
+						},
+					},
 				},
 				{
 					name:  "default",
@@ -1259,7 +1552,7 @@ func TestDecodeParameter(t *testing.T) {
 
 					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)
+						matchParseError(t, err, tc.err)
 
 						return
 					}
@@ -1486,7 +1779,7 @@ func TestDecodeBody(t *testing.T) {
 
 			if tc.wantErr != nil {
 				require.Error(t, err)
-				require.Truef(t, matchParseError(err, tc.wantErr), "got error:\n%v\nwant error:\n%v", err, tc.wantErr)
+				matchParseError(t, err, tc.wantErr)
 				return
 			}
 
@@ -1569,26 +1862,27 @@ func TestRegisterAndUnregisterBodyDecoder(t *testing.T) {
 	}, err)
 }
 
-func matchParseError(got, want error) bool {
+func matchParseError(t *testing.T, got, want error) {
+	t.Helper()
+
 	wErr, ok := want.(*ParseError)
 	if !ok {
-		return false
+		t.Errorf("want error is not a ParseError")
+		return
 	}
 	gErr, ok := got.(*ParseError)
 	if !ok {
-		return false
-	}
-	if wErr.Kind != gErr.Kind {
-		return false
+		t.Errorf("got error is not a ParseError")
+		return
 	}
-	if !reflect.DeepEqual(wErr.Value, gErr.Value) {
-		return false
-	}
-	if !reflect.DeepEqual(wErr.Path(), gErr.Path()) {
-		return false
+	assert.Equalf(t, wErr.Kind, gErr.Kind, "ParseError Kind differs")
+	assert.Equalf(t, wErr.Value, gErr.Value, "ParseError Value differs")
+	assert.Equalf(t, wErr.Path(), gErr.Path(), "ParseError Path differs")
+
+	if wErr.Reason != "" {
+		assert.Equalf(t, wErr.Reason, gErr.Reason, "ParseError Reason differs")
 	}
 	if wErr.Cause != nil {
-		return matchParseError(gErr.Cause, wErr.Cause)
+		matchParseError(t, gErr.Cause, wErr.Cause)
 	}
-	return true
 }
diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go
index 1322a55..14f568d 100644
--- a/openapi3filter/validate_request_test.go
+++ b/openapi3filter/validate_request_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/getkin/kin-openapi/openapi3"
 	"github.com/getkin/kin-openapi/routers"
 	"github.com/getkin/kin-openapi/routers/gorillamux"
+	legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
 )
 
 func setupTestRouter(t *testing.T, spec string) routers.Router {
@@ -222,6 +223,307 @@ components:
 	}
 }
 
+func TestValidateQueryParams(t *testing.T) {
+	type testCase struct {
+		name  string
+		param *openapi3.Parameter
+		query string
+		want  map[string]interface{}
+		err   *openapi3.SchemaError // test ParseError in decoder tests
+	}
+
+	testCases := []testCase{
+		{
+			name: "deepObject explode additionalProperties with object properties - missing required property",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", additionalPropertiesObjectOf(func() *openapi3.SchemaRef {
+						s := objectOf(
+							"item1", integerSchema,
+							"requiredProp", stringSchema,
+						)
+						s.Value.Required = []string{"requiredProp"}
+
+						return s
+					}()),
+					"objIgnored", objectOf("items", stringArraySchema),
+				),
+			},
+			query: "param[obj][prop1][item1]=1",
+			err:   &openapi3.SchemaError{SchemaField: "required", Reason: "property \"requiredProp\" is missing"},
+		},
+		{
+			// XXX should this error out?
+			name: "deepObject explode additionalProperties with object properties - extraneous nested param property ignored",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", additionalPropertiesObjectOf(objectOf(
+						"item1", integerSchema,
+						"requiredProp", stringSchema,
+					)),
+					"objIgnored", objectOf("items", stringArraySchema),
+				),
+			},
+			query: "param[obj][prop1][inexistent]=1",
+			want: map[string]interface{}{
+				"obj": map[string]interface{}{
+					"prop1": map[string]interface{}{},
+				},
+			},
+		},
+		{
+			name: "deepObject explode additionalProperties with object properties",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", additionalPropertiesObjectOf(objectOf(
+						"item1", numberSchema,
+						"requiredProp", stringSchema,
+					)),
+					"objIgnored", objectOf("items", stringArraySchema),
+				),
+			},
+			query: "param[obj][prop1][item1]=1.123",
+			want: map[string]interface{}{
+				"obj": map[string]interface{}{
+					"prop1": map[string]interface{}{
+						"item1": float64(1.123),
+					},
+				},
+			},
+		},
+		{
+			name: "deepObject explode nested objects - misplaced parameter",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", objectOf("nestedObjOne", objectOf("items", stringArraySchema)),
+				),
+			},
+			query: "param[obj][nestedObjOne]=baz",
+			err: &openapi3.SchemaError{
+				SchemaField: "type", Reason: "value must be an object", Value: "baz", Schema: objectOf("items", stringArraySchema).Value,
+			},
+		},
+		{
+			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: "anotherparam=bar",
+			want:  map[string]interface{}(nil),
+		},
+		{
+			name: "deepObject explode additionalProperties with object properties - multiple properties",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", additionalPropertiesObjectOf(objectOf("item1", integerSchema, "item2", stringArraySchema)),
+					"objIgnored", objectOf("items", stringArraySchema),
+				),
+			},
+			query: "param[obj][prop1][item1]=1&param[obj][prop1][item2][0]=abc&param[obj][prop2][item1]=2&param[obj][prop2][item2][0]=def",
+			want: map[string]interface{}{
+				"obj": map[string]interface{}{
+					"prop1": map[string]interface{}{
+						"item1": int64(1),
+						"item2": []interface{}{"abc"},
+					},
+					"prop2": map[string]interface{}{
+						"item1": int64(2),
+						"item2": []interface{}{"def"},
+					},
+				},
+			},
+		},
+
+		//
+		//
+		{
+			name: "deepObject explode nested object anyOf",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", anyofSchema,
+				),
+			},
+			query: "param[obj]=1",
+			want: map[string]interface{}{
+				"obj": int64(1),
+			},
+		},
+		{
+			name: "deepObject explode nested object allOf",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", allofSchema,
+				),
+			},
+			query: "param[obj]=1",
+			want: map[string]interface{}{
+				"obj": int64(1),
+			},
+		},
+		{
+			name: "deepObject explode nested object oneOf",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", oneofSchema,
+				),
+			},
+			query: "param[obj]=true",
+			want: map[string]interface{}{
+				"obj": true,
+			},
+		},
+		{
+			name: "deepObject explode nested object oneOf - object",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", oneofSchemaObject,
+				),
+			},
+			query: "param[obj][id2]=1&param[obj][name2]=abc",
+			want: map[string]interface{}{
+				"obj": map[string]interface{}{
+					"id2":   "1",
+					"name2": "abc",
+				},
+			},
+		},
+		{
+			name: "deepObject explode nested object oneOf - object - more than one match",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", oneofSchemaObject,
+				),
+			},
+			query: "param[obj][id]=1&param[obj][id2]=2",
+			err: &openapi3.SchemaError{
+				SchemaField: "oneOf",
+				Value:       map[string]interface{}{"id": "1", "id2": "2"},
+				Reason:      "value matches more than one schema from \"oneOf\" (matches schemas at indices [0 1])",
+				Schema:      oneofSchemaObject.Value,
+			},
+		},
+		{
+			name: "deepObject explode nested object oneOf - array",
+			param: &openapi3.Parameter{
+				Name: "param", In: "query", Style: "deepObject", Explode: explode,
+				Schema: objectOf(
+					"obj", oneofSchemaArrayObject,
+				),
+			},
+			query: "param[obj][0]=a&param[obj][1]=b",
+			want: map[string]interface{}{
+				"obj": []interface{}{
+					"a",
+					"b",
+				},
+			},
+		},
+		//
+		//
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			info := &openapi3.Info{
+				Title:   "MyAPI",
+				Version: "0.1",
+			}
+			doc := &openapi3.T{OpenAPI: "3.0.0", Info: info, Paths: openapi3.NewPaths()}
+			op := &openapi3.Operation{
+				OperationID: "test",
+				Parameters:  []*openapi3.ParameterRef{{Value: tc.param}},
+				Responses:   openapi3.NewResponses(),
+			}
+			doc.AddOperation("/test", http.MethodGet, op)
+			err := doc.Validate(context.Background())
+			require.NoError(t, err)
+			router, err := legacyrouter.NewRouter(doc)
+			require.NoError(t, err)
+
+			req, err := http.NewRequest(http.MethodGet, "http://test.org/test?"+tc.query, nil)
+			route, pathParams, err := router.FindRoute(req)
+			require.NoError(t, err)
+
+			input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route}
+			err = ValidateParameter(context.Background(), input, tc.param)
+
+			if tc.err != nil {
+				require.Error(t, err)
+				re, ok := err.(*RequestError)
+				if !ok {
+					t.Errorf("error is not a RequestError")
+
+					return
+				}
+
+				gErr, ok := re.Unwrap().(*openapi3.SchemaError)
+				if !ok {
+					t.Errorf("unknown RequestError wrapped error type")
+				}
+				matchSchemaError(t, gErr, tc.err)
+
+				return
+			}
+
+			require.NoError(t, err)
+
+			got, _, err := decodeStyledParameter(tc.param, input)
+			require.EqualValues(t, tc.want, got)
+		})
+	}
+}
+
+func matchSchemaError(t *testing.T, got, want error) {
+	t.Helper()
+
+	wErr, ok := want.(*openapi3.SchemaError)
+	if !ok {
+		t.Errorf("want error is not a SchemaError")
+		return
+	}
+	gErr, ok := got.(*openapi3.SchemaError)
+	if !ok {
+		t.Errorf("got error is not a SchemaError")
+		return
+	}
+	assert.Equalf(t, wErr.SchemaField, gErr.SchemaField, "SchemaError SchemaField differs")
+	assert.Equalf(t, wErr.Reason, gErr.Reason, "SchemaError Reason differs")
+
+	if wErr.Schema != nil {
+		assert.EqualValuesf(t, wErr.Schema, gErr.Schema, "SchemaError Schema differs")
+	}
+	if wErr.Value != nil {
+		assert.EqualValuesf(t, wErr.Value, gErr.Value, "SchemaError Value differs")
+	}
+
+	if gErr.Origin == nil && wErr.Origin != nil {
+		t.Errorf("expected error origin but got nothing")
+	}
+	if gErr.Origin != nil && wErr.Origin != nil {
+		switch gErrOrigin := gErr.Origin.(type) {
+		case *openapi3.SchemaError:
+			matchSchemaError(t, gErrOrigin, wErr.Origin)
+		case *ParseError:
+			matchParseError(t, gErrOrigin, wErr.Origin)
+		default:
+			t.Errorf("unknown origin error")
+		}
+	}
+}
+
 func TestValidateRequestExcludeQueryParams(t *testing.T) {
 	const spec = `
 openapi: 3.0.0
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
index 6fa7948..f8fd6db 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -194,16 +194,16 @@ func getValidationTests(t *testing.T) []*validationTest {
 		{
 			name: "deepobject query parameter type",
 			args: validationArgs{
-				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[booleans]=true&deepFilter[integers]=1&deepFilter[strings]=foo%26o&deepFilter[numbers]=1", nil),
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[booleans][0]=true&deepFilter[integers][0]=1&deepFilter[strings][0]=foo%26o&deepFilter[numbers][0]=1", nil),
 			},
 		},
 		{
 			name: "error - incorrect deepobject query parameter type bool",
 			args: validationArgs{
-				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[booleans]=notbool", nil),
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[booleans][0]=notbool", nil),
 			},
 			wantErrParam:   "deepFilter",
-			wantErrBody:    `parameter "deepFilter" in query has an error: path booleans: value notbool: an invalid boolean: invalid syntax`,
+			wantErrBody:    "parameter \"deepFilter\" in query has an error: path booleans.0: value notbool: an invalid boolean: invalid syntax",
 			wantErrParamIn: "query",
 			wantErrResponse: &ValidationError{
 				Status: http.StatusBadRequest,
@@ -213,10 +213,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 		{
 			name: "error - incorrect deepobject query parameter type integer",
 			args: validationArgs{
-				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[integers]=1.234", nil),
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[integers][0]=1.234", nil),
 			},
 			wantErrParam:   "deepFilter",
-			wantErrBody:    `parameter "deepFilter" in query has an error: path integers: value 1.234: an invalid integer: invalid syntax`,
+			wantErrBody:    "parameter \"deepFilter\" in query has an error: path integers.0: value 1.234: an invalid integer: invalid syntax",
 			wantErrParamIn: "query",
 			wantErrResponse: &ValidationError{
 				Status: http.StatusBadRequest,
@@ -224,12 +224,12 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 		},
 		{
-			name: "error - incorrect deepobject query parameter type integer",
+			name: "error - incorrect deepobject query parameter type number",
 			args: validationArgs{
-				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[numbers]=aaa", nil),
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[numbers][0]=aaa", nil),
 			},
 			wantErrParam:   "deepFilter",
-			wantErrBody:    `parameter "deepFilter" in query has an error: path numbers: value aaa: an invalid number: invalid syntax`,
+			wantErrBody:    "parameter \"deepFilter\" in query has an error: path numbers.0: value aaa: an invalid number: invalid syntax",
 			wantErrParamIn: "query",
 			wantErrResponse: &ValidationError{
 				Status: http.StatusBadRequest,
@@ -633,7 +633,7 @@ func TestValidationErrorEncoder(t *testing.T) {
 			req.NoError(err)
 
 			err = h.validateRequest(tt.args.r)
-			req.Equal(tt.wantErr, err != nil)
+			req.Equal(tt.wantErr, err != nil, "wantError: %v", tt.wantErr)
 
 			if err != nil {
 				encoder.Encode(tt.args.r.Context(), err, httptest.NewRecorder())