Repository: getkin/kin-openapi
Pull request #920 (2024-03-06 04:53:49)

openapi3gen: allow overriding how a Schema is generated
Implementation of #919 


Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 8090952..1ae2093 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -134,6 +134,9 @@ type AdditionalProperties struct {
 func (addProps AdditionalProperties) MarshalJSON() ([]byte, error)
     MarshalJSON returns the JSON encoding of AdditionalProperties.
 
+func (addProps AdditionalProperties) MarshalYAML() (interface{}, error)
+    MarshalYAML returns the YAML encoding of AdditionalProperties.
+
 func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets AdditionalProperties to a copy of data.
 
diff --git a/.github/docs/openapi3gen.txt b/.github/docs/openapi3gen.txt
index 9cf293c..e692b85 100644
--- a/.github/docs/openapi3gen.txt
+++ b/.github/docs/openapi3gen.txt
@@ -70,3 +70,10 @@ type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag,
     A SchemaCustomizerFn can return an ExcludeSchemaSentinel error to indicate
     that the schema for this field should not be included in the final output
 
+type SetSchemar interface {
+	SetSchema(*openapi3.Schema)
+}
+    SetSchemar allows client to set their own schema definition according to
+    their specification. Useful when some custom datatype is needed and/or some
+    custom logic is needed on how the schema values would be generated
+
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 32027a1..ae28afe 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -213,6 +213,20 @@ type AdditionalProperties struct {
 	Schema *SchemaRef
 }
 
+// MarshalYAML returns the YAML encoding of AdditionalProperties.
+func (addProps AdditionalProperties) MarshalYAML() (interface{}, error) {
+	if x := addProps.Has; x != nil {
+		if *x {
+			return true, nil
+		}
+		return false, nil
+	}
+	if x := addProps.Schema; x != nil {
+		return x.Value, nil
+	}
+	return nil, nil
+}
+
 // MarshalJSON returns the JSON encoding of AdditionalProperties.
 func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) {
 	if x := addProps.Has; x != nil {
@@ -1737,7 +1751,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 				}
 			case f.regexp == nil && f.callback != nil:
 				if err := f.callback(value); err != nil {
-					var schemaErr = &SchemaError{}
+					schemaErr := &SchemaError{}
 					if errors.As(err, &schemaErr) {
 						formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%s)`, format, schemaErr.Reason)
 					} else {
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index 60afc54..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,71 +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{})
+
+	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 result, nil
+}
+
+// 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 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
 			}
-			obj[propName] = vals
-		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 r != nil {
+				resultArr[i] = r
+			}
+		}
+		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
+			}
+			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, &ParseError{path: pathFromKeys(mapKeys), Reason: err.Error()}
+					return nil, err
 				}
-				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)
-						}
-					}
-					deepSet(obj, mapKeys, vals)
-					continue
+				if r != nil {
+					resultMap[k] = r
 				}
-				value, err := parsePrimitive(props[prop], nestedSchema)
-				if err != nil {
-					return nil, handlePropParseError(mapKeys, err)
+			}
+		}
+
+		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"}
+		}
+		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
 				}
-				deepSet(obj, mapKeys, value)
+				continue
 			}
-		default:
-			value, err := parsePrimitive(props[propName], propSchema)
-			if err != nil {
-				return nil, handlePropParseError([]string{propName}, err)
+
+			if a, ok := val.([]interface{}); ok {
+				if len(a) > 0 {
+					return a, nil
+				}
+				continue
 			}
-			obj[propName] = value
+
+			// if its a primitive and not nil just return that and let it be validated
+			return val, nil
 		}
 	}
-	return obj, nil
+
+	if len(resultMap) > 0 {
+		return resultMap, nil
+	}
+
+	return nil, nil
 }
 
 func handlePropParseError(path []string, err error) error {
diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go
index 95805ee..a301337 100644
--- a/openapi3gen/openapi3gen.go
+++ b/openapi3gen/openapi3gen.go
@@ -35,6 +35,13 @@ type Option func(*generatorOpt)
 // the final output
 type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error
 
+// SetSchemar allows client to set their own schema definition according to
+// their specification. Useful when some custom datatype is needed and/or some custom logic
+// is needed on how the schema values would be generated
+type SetSchemar interface {
+	SetSchema(*openapi3.Schema)
+}
+
 type generatorOpt struct {
 	useAllExportedFields bool
 	throwErrorOnCycle    bool
@@ -347,6 +354,14 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
 				schema.Type = &openapi3.Types{"object"}
 			}
 		}
+
+	default:
+		// Object has their own schema's implementation, so we'll use those
+		if v := reflect.New(t); v.CanInterface() {
+			if v, ok := v.Interface().(SetSchemar); ok {
+				v.SetSchema(schema)
+			}
+		}
 	}
 
 	if g.opts.schemaCustomizer != nil {


Test patch:
diff --git a/openapi3/additionalProperties_test.go b/openapi3/additionalProperties_test.go
new file mode 100644
index 0000000..7a9ec74
--- /dev/null
+++ b/openapi3/additionalProperties_test.go
@@ -0,0 +1,40 @@
+package openapi3_test
+
+import (
+	"bytes"
+	"context"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+func TestMarshalAdditionalProperties(t *testing.T) {
+	ctx := context.Background()
+	data, err := os.ReadFile("testdata/test.openapi.additionalproperties.yml")
+	require.NoError(t, err)
+
+	loader := openapi3.NewLoader()
+	loader.IsExternalRefsAllowed = true
+	spec, err := loader.LoadFromData(data)
+	require.NoError(t, err)
+
+	err = spec.Validate(ctx)
+	require.NoError(t, err)
+
+	var buf bytes.Buffer
+	enc := yaml.NewEncoder(&buf)
+	enc.SetIndent(2)
+	err = enc.Encode(spec)
+	require.NoError(t, err)
+
+	// Load the doc from the serialized yaml.
+	spec2, err := loader.LoadFromData(buf.Bytes())
+	require.NoError(t, err)
+
+	err = spec2.Validate(ctx)
+	require.NoError(t, err)
+}
diff --git a/openapi3/testdata/test.openapi.additionalproperties.yml b/openapi3/testdata/test.openapi.additionalproperties.yml
new file mode 100644
index 0000000..cfd078a
--- /dev/null
+++ b/openapi3/testdata/test.openapi.additionalproperties.yml
@@ -0,0 +1,17 @@
+openapi: 3.0.0
+info:
+  title: "OAI Specification in YAML"
+  version: 0.0.1
+paths: {}
+components:
+  schemas:
+    TestSchema:
+      type: object
+      additionalProperties:
+        type: array
+        items:
+          type: string
+    TestSchema2:
+      type: object
+      additionalProperties:
+        type: string
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/testdata/fixtures/petstore.json b/openapi3filter/testdata/fixtures/petstore.json
index 1a8cd8c..7d3c97a 100644
--- a/openapi3filter/testdata/fixtures/petstore.json
+++ b/openapi3filter/testdata/fixtures/petstore.json
@@ -140,6 +140,98 @@
         }
       }
     },
+    "/pet/filter": {
+      "get": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Finds Pets by status",
+        "operationId": "filterPets",
+        "parameters": [
+          {
+            "in": "query",
+            "name": "deepFilter",
+            "style": "deepObject",
+            "explode": true,
+            "allowReserved": true,
+            "schema": {
+              "type": "object",
+              "properties": {
+                "strings": {
+                  "type": "array",
+                  "items": {
+                    "type": "string"
+                  }
+                },
+                "booleans": {
+                  "type": "array",
+                  "items": {
+                    "type": "boolean"
+                  }
+                },
+                "numbers": {
+                  "type": "array",
+                  "items": {
+                    "type": "number"
+                  }
+                },
+                "integers": {
+                  "type": "array",
+                  "items": {
+                    "type": "integer"
+                  }
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "allOf": [
+                      {
+                        "$ref": "#/components/schemas/Pet"
+                      },
+                      {
+                        "$ref": "#/components/schemas/PetRequiredProperties"
+                      }
+                    ]
+                  }
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "allOf": [
+                      {
+                        "$ref": "#/components/schemas/Pet"
+                      },
+                      {
+                        "$ref": "#/components/schemas/PetRequiredProperties"
+                      }
+                    ]
+                  }
+                }
+              }
+            }
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ]
+      }
+    },
     "/pet/findByStatus": {
       "get": {
         "tags": [
@@ -178,8 +270,12 @@
                   "type": "array",
                   "items": {
                     "allOf": [
-                      {"$ref": "#/components/schemas/Pet"},
-                      {"$ref": "#/components/schemas/PetRequiredProperties"}
+                      {
+                        "$ref": "#/components/schemas/Pet"
+                      },
+                      {
+                        "$ref": "#/components/schemas/PetRequiredProperties"
+                      }
                     ]
                   }
                 }
@@ -189,8 +285,12 @@
                   "type": "array",
                   "items": {
                     "allOf": [
-                      {"$ref": "#/components/schemas/Pet"},
-                      {"$ref": "#/components/schemas/PetRequiredProperties"}
+                      {
+                        "$ref": "#/components/schemas/Pet"
+                      },
+                      {
+                        "$ref": "#/components/schemas/PetRequiredProperties"
+                      }
                     ]
                   }
                 }
@@ -283,8 +383,12 @@
                   "type": "array",
                   "items": {
                     "allOf": [
-                      {"$ref": "#/components/schemas/Pet"},
-                      {"$ref": "#/components/schemas/PetRequiredProperties"}
+                      {
+                        "$ref": "#/components/schemas/Pet"
+                      },
+                      {
+                        "$ref": "#/components/schemas/PetRequiredProperties"
+                      }
                     ]
                   }
                 }
@@ -294,8 +398,12 @@
                   "type": "array",
                   "items": {
                     "allOf": [
-                      {"$ref": "#/components/schemas/Pet"},
-                      {"$ref": "#/components/schemas/PetRequiredProperties"}
+                      {
+                        "$ref": "#/components/schemas/Pet"
+                      },
+                      {
+                        "$ref": "#/components/schemas/PetRequiredProperties"
+                      }
                     ]
                   }
                 }
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 70b46c7..f8fd6db 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -108,8 +108,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrReason: routers.ErrMethodNotAllowed.Error(),
 			// TODO: By HTTP spec, this should have an Allow header with what is allowed
 			// but kin-openapi doesn't provide us the requested method or path, so impossible to provide details
-			wantErrResponse: &ValidationError{Status: http.StatusMethodNotAllowed,
-				Title: routers.ErrMethodNotAllowed.Error()},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusMethodNotAllowed,
+				Title:  routers.ErrMethodNotAllowed.Error(),
+			},
 		},
 		{
 			name: "error - missing body on POST",
@@ -117,8 +119,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 				r: missingBody1,
 			},
 			wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(),
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: "request body has an error: " + ErrInvalidRequired.Error()},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  "request body has an error: " + ErrInvalidRequired.Error(),
+			},
 		},
 		{
 			name: "error - empty body on POST",
@@ -126,8 +130,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 				r: missingBody2,
 			},
 			wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(),
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: "request body has an error: " + ErrInvalidRequired.Error()},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  "request body has an error: " + ErrInvalidRequired.Error(),
+			},
 		},
 
 		//
@@ -140,8 +146,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 				r: noContentType,
 			},
 			wantErrReason: prefixInvalidCT + ` ""`,
-			wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
-				Title: "header Content-Type is required"},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnsupportedMediaType,
+				Title:  "header Content-Type is required",
+			},
 		},
 		{
 			name: "error - unknown content-type on POST",
@@ -151,8 +159,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrReason:      "failed to decode request body",
 			wantErrParseKind:   KindUnsupportedFormat,
 			wantErrParseReason: prefixUnsupportedCT + ` "application/xml"`,
-			wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
-				Title: prefixUnsupportedCT + ` "application/xml"`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnsupportedMediaType,
+				Title:  prefixUnsupportedCT + ` "application/xml"`,
+			},
 		},
 		{
 			name: "error - unsupported content-type on POST",
@@ -160,8 +170,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 				r: unsupportedContentType,
 			},
 			wantErrReason: prefixInvalidCT + ` "text/plain"`,
-			wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
-				Title: prefixUnsupportedCT + ` "text/plain"`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnsupportedMediaType,
+				Title:  prefixUnsupportedCT + ` "text/plain"`,
+			},
 		},
 		{
 			name: "success - no content-type header required on GET",
@@ -173,7 +185,57 @@ func getValidationTests(t *testing.T) []*validationTest {
 		//
 		// Query strings
 		//
-
+		{
+			name: "empty deepobject query parameter",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter", nil),
+			},
+		},
+		{
+			name: "deepobject query parameter type",
+			args: validationArgs{
+				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][0]=notbool", nil),
+			},
+			wantErrParam:   "deepFilter",
+			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,
+				Title:  "parameter \"deepFilter\" in query is invalid: notbool is an invalid boolean",
+			},
+		},
+		{
+			name: "error - incorrect deepobject query parameter type integer",
+			args: validationArgs{
+				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.0: value 1.234: an invalid integer: invalid syntax",
+			wantErrParamIn: "query",
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  "parameter \"deepFilter\" in query is invalid: 1.234 is an invalid integer",
+			},
+		},
+		{
+			name: "error - incorrect deepobject query parameter type number",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/filter?deepFilter[numbers][0]=aaa", nil),
+			},
+			wantErrParam:   "deepFilter",
+			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,
+				Title:  "parameter \"deepFilter\" in query is invalid: aaa is an invalid number",
+			},
+		},
 		{
 			name: "error - missing required query string parameter",
 			args: validationArgs{
@@ -183,8 +245,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrParamIn: "query",
 			wantErrBody:    `parameter "status" in query has an error: value is required but missing`,
 			wantErrReason:  "value is required but missing",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: `parameter "status" in query is required`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  `parameter "status" in query is required`,
+			},
 		},
 		{
 			name: "error - wrong query string parameter type as integer",
@@ -197,8 +261,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			// So we'd need to look at the inner one which is a KindInvalidFormat. So just check the error body.
 			wantErrBody: `parameter "ids" in query has an error: path 1: value notAnInt: an invalid integer: invalid syntax`,
 			// TODO: Should we treat query params of the wrong type like a 404 instead of a 400?
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: `parameter "ids" in query is invalid: notAnInt is an invalid integer`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  `parameter "ids" in query is invalid: notAnInt is an invalid integer`,
+			},
 		},
 		{
 			name: "success - ignores unknown query string parameter",
@@ -215,8 +281,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrParamIn: "query",
 			wantErrBody:    `parameter "tags" in query has an error: empty value is not allowed`,
 			wantErrReason:  "empty value is not allowed",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: `parameter "tags" in query is not allowed to be empty`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  `parameter "tags" in query is not allowed to be empty`,
+			},
 		},
 		{
 			name: "success - non required query string has empty value, but has AllowEmptyValue",
@@ -246,13 +314,15 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
 			wantErrSchemaPath:   "/0",
 			wantErrSchemaValue:  "available,sold",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
 				Detail: "value available,sold at /0 must be one of: available, pending, sold; " +
 					// TODO: do we really want to use this heuristic to guess
 					//  that they're using the wrong serialization?
 					"perhaps you intended '?status=available&status=sold'",
-				Source: &ValidationErrorSource{Parameter: "status"}},
+				Source: &ValidationErrorSource{Parameter: "status"},
+			},
 		},
 		{
 			name: "error - invalid enum value for query string parameter",
@@ -264,10 +334,12 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
 			wantErrSchemaPath:   "/1",
 			wantErrSchemaValue:  "watdis",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
 				Title:  "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
 				Detail: "value watdis at /1 must be one of: available, pending, sold",
-				Source: &ValidationErrorSource{Parameter: "status"}},
+				Source: &ValidationErrorSource{Parameter: "status"},
+			},
 		},
 		{
 			name: "error - invalid enum value, allowing commas (without 'perhaps you intended' recommendation)",
@@ -280,11 +352,13 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: "value is not one of the allowed values [\"dog\",\"cat\",\"turtle\",\"bird,with,commas\"]",
 			wantErrSchemaPath:   "/1",
 			wantErrSchemaValue:  "fish,with,commas",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
 				Title:  "value is not one of the allowed values [\"dog\",\"cat\",\"turtle\",\"bird,with,commas\"]",
 				Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas",
 				// No 'perhaps you intended' because its the right serialization format
-				Source: &ValidationErrorSource{Parameter: "kind"}},
+				Source: &ValidationErrorSource{Parameter: "kind"},
+			},
 		},
 		{
 			name: "success - valid enum value, allowing commas",
@@ -306,10 +380,12 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: "value is not one of the allowed values [\"demo\",\"prod\"]",
 			wantErrSchemaPath:   "/",
 			wantErrSchemaValue:  "watdis",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
 				Title:  "value is not one of the allowed values [\"demo\",\"prod\"]",
 				Detail: "value watdis at / must be one of: demo, prod",
-				Source: &ValidationErrorSource{Parameter: "x-environment"}},
+				Source: &ValidationErrorSource{Parameter: "x-environment"},
+			},
 		},
 
 		//
@@ -325,10 +401,12 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
 			wantErrSchemaValue:  "watdis",
 			wantErrSchemaPath:   "/status",
-			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnprocessableEntity,
 				Title:  "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
 				Detail: "value watdis at /status must be one of: available, pending, sold",
-				Source: &ValidationErrorSource{Pointer: "/status"}},
+				Source: &ValidationErrorSource{Pointer: "/status"},
+			},
 		},
 		{
 			name: "error - missing required object attribute",
@@ -339,9 +417,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: `property "photoUrls" is missing`,
 			wantErrSchemaValue:  map[string]string{"name": "Bahama"},
 			wantErrSchemaPath:   "/photoUrls",
-			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnprocessableEntity,
 				Title:  `property "photoUrls" is missing`,
-				Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
+				Source: &ValidationErrorSource{Pointer: "/photoUrls"},
+			},
 		},
 		{
 			name: "error - missing required nested object attribute",
@@ -353,9 +433,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: `property "name" is missing`,
 			wantErrSchemaValue:  map[string]string{},
 			wantErrSchemaPath:   "/category/name",
-			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnprocessableEntity,
 				Title:  `property "name" is missing`,
-				Source: &ValidationErrorSource{Pointer: "/category/name"}},
+				Source: &ValidationErrorSource{Pointer: "/category/name"},
+			},
 		},
 		{
 			name: "error - missing required deeply nested object attribute",
@@ -367,9 +449,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaReason: `property "name" is missing`,
 			wantErrSchemaValue:  map[string]string{},
 			wantErrSchemaPath:   "/category/tags/0/name",
-			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnprocessableEntity,
 				Title:  `property "name" is missing`,
-				Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}},
+				Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"},
+			},
 		},
 		{
 			name: "error - wrong attribute type",
@@ -383,9 +467,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaValue:  "http://cat",
 			// TODO: this shouldn't say "or not be present", but this requires recursively resolving
 			//  innerErr.JSONPointer() against e.RequestBody.Content["application/json"].Schema.Value (.Required, .Properties)
-			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnprocessableEntity,
 				Title:  "value must be an array",
-				Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
+				Source: &ValidationErrorSource{Pointer: "/photoUrls"},
+			},
 		},
 		{
 			name: "error - missing required object attribute from allOf required overlay",
@@ -399,9 +485,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrSchemaOriginReason: `property "photoUrls" is missing`,
 			wantErrSchemaOriginValue:  map[string]string{"name": "Bahama"},
 			wantErrSchemaOriginPath:   "/photoUrls",
-			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+			wantErrResponse: &ValidationError{
+				Status: http.StatusUnprocessableEntity,
 				Title:  `property "photoUrls" is missing`,
-				Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
+				Source: &ValidationErrorSource{Pointer: "/photoUrls"},
+			},
 		},
 		{
 			name: "success - ignores unknown object attribute",
@@ -437,8 +525,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrParamIn: "path",
 			wantErrBody:    `parameter "petId" in path has an error: value is required but missing`,
 			wantErrReason:  "value is required but missing",
-			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: `parameter "petId" in path is required`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  `parameter "petId" in path is required`,
+			},
 		},
 		{
 			name: "error - wrong path param type",
@@ -450,8 +540,10 @@ func getValidationTests(t *testing.T) []*validationTest {
 			wantErrParseKind:   KindInvalidFormat,
 			wantErrParseValue:  "NotAnInt",
 			wantErrParseReason: "an invalid integer",
-			wantErrResponse: &ValidationError{Status: http.StatusNotFound,
-				Title: `resource not found with "petId" value: NotAnInt`},
+			wantErrResponse: &ValidationError{
+				Status: http.StatusNotFound,
+				Title:  `resource not found with "petId" value: NotAnInt`,
+			},
 		},
 		{
 			name: "success - normal case, with path params",
@@ -470,7 +562,6 @@ func TestValidationHandler_validateRequest(t *testing.T) {
 
 			h, err := buildValidationHandler(tt)
 			req.NoError(err)
-
 			err = h.validateRequest(tt.args.r)
 			req.Equal(tt.wantErr, err != nil)
 
@@ -542,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())
diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go
index daea9ff..bb9ea4d 100644
--- a/openapi3gen/openapi3gen_test.go
+++ b/openapi3gen/openapi3gen_test.go
@@ -590,3 +590,43 @@ func ExampleNewSchemaRefForValue_recursive() {
 	//   "type": "object"
 	// }
 }
+
+type ID [16]byte
+
+// T implements SetSchemar, allowing it to set an OpenAPI schema.
+type T struct {
+	ID ID `json:"id"`
+}
+
+func (_ *ID) SetSchema(schema *openapi3.Schema) {
+	schema.Type = &openapi3.Types{"string"} // Assuming this matches your custom implementation
+	schema.Format = "uuid"
+}
+
+func ExampleID_SetSchema() {
+	schemas := make(openapi3.Schemas)
+	instance := &T{
+		ID: ID{},
+	}
+
+	// Generate the schema for the instance
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(instance, schemas)
+	if err != nil {
+		panic(err)
+	}
+	data, err := json.MarshalIndent(schemaRef, "", "  ")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	// Output:
+	// schemaRef: {
+	//   "properties": {
+	//     "id": {
+	//       "format": "uuid",
+	//       "type": "string"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
+}