Repository: getkin/kin-openapi
Pull request #197 (2020-03-28 16:27:34)

Add validation handler and error encoder
Implement https://github.com/getkin/kin-openapi/issues/194 and https://github.com/getkin/kin-openapi/issues/195

This is what I had to add on top of kin-openapi in order to provide openapi validation errors in my API service. More discussion at https://github.com/getkin/kin-openapi/issues/194#issuecomment-605488953


Patch:
diff --git a/go.mod b/go.mod
index 9f441f2..2301383 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,9 @@
 module github.com/getkin/kin-openapi
 
+go 1.14
+
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/ghodss/yaml v1.0.0
-	github.com/kr/pretty v0.1.0 // indirect
-	github.com/stretchr/testify v1.3.0
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
+	github.com/stretchr/testify v1.5.1
 	gopkg.in/yaml.v2 v2.2.8 // indirect
 )
-
-go 1.13
diff --git a/go.sum b/go.sum
index 583c172..22c1b57 100644
--- a/go.sum
+++ b/go.sum
@@ -1,21 +1,13 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/jsoninfo/type_info.go b/jsoninfo/type_info.go
index 5d7a4ed..3dbb8d5 100644
--- a/jsoninfo/type_info.go
+++ b/jsoninfo/type_info.go
@@ -60,9 +60,9 @@ func GetTypeInfo(t reflect.Type) *TypeInfo {
 // FieldNames returns all field names
 func (typeInfo *TypeInfo) FieldNames() []string {
 	fields := typeInfo.Fields
-	names := make([]string, len(fields))
-	for i, field := range fields {
-		names[i] = field.JSONName
+	names := make([]string, 0, len(fields))
+	for _, field := range fields {
+		names = append(names, field.JSONName)
 	}
 	return names
 }
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index 63f9728..6e0b10e 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -11,6 +11,7 @@ import (
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
+// ToV3Swagger converts an OpenAPIv2 spec to an OpenAPIv3 spec
 func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 	result := &openapi3.Swagger{
 		OpenAPI:    "3.0.2",
@@ -156,6 +157,11 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 	if parameter == nil {
 		return nil, nil, nil
 	}
+	if ref := parameter.Ref; len(ref) > 0 {
+		return &openapi3.ParameterRef{
+			Ref: ToV3Ref(ref),
+		}, nil, nil
+	}
 	in := parameter.In
 	if in == "body" {
 		result := &openapi3.RequestBody{
@@ -209,7 +215,7 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
 		}, nil
 	}
 	result := &openapi3.Response{
-		Description: response.Description,
+		Description: &response.Description,
 	}
 	if schemaRef := response.Schema; schemaRef != nil {
 		result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
@@ -251,6 +257,7 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
 var ref2To3 = map[string]string{
 	"#/definitions/": "#/components/schemas/",
 	"#/responses/":   "#/components/responses/",
+	"#/parameters/":  "#/components/parameters/",
 }
 
 func ToV3Ref(ref string) string {
@@ -367,6 +374,7 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 		if pathItem == nil {
 			continue
 		}
+		result.AddOperation(path, "GET", nil)
 		for method, operation := range pathItem.Operations() {
 			if operation == nil {
 				continue
@@ -377,6 +385,21 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 			}
 			result.AddOperation(path, method, resultOperation)
 		}
+		params := openapi2.Parameters{}
+		for _, param := range pathItem.Parameters {
+			p, err := FromV3Parameter(param)
+			if err != nil {
+				return nil, err
+			}
+			params = append(params, p)
+		}
+		result.Paths[path].Parameters = params
+	}
+	result.Parameters = map[string]*openapi2.Parameter{}
+	for name, param := range swagger.Components.Parameters {
+		if result.Parameters[name], err = FromV3Parameter(param); err != nil {
+			return nil, err
+		}
 	}
 	if m := swagger.Components.SecuritySchemes; m != nil {
 		resultSecuritySchemes := make(map[string]*openapi2.SecurityScheme)
@@ -594,8 +617,12 @@ func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
 	if response == nil {
 		return nil, nil
 	}
+	description := ""
+	if desc := response.Description; desc != nil {
+		description = *desc
+	}
 	result := &openapi2.Response{
-		Description: response.Description,
+		Description: description,
 	}
 	if content := response.Content; content != nil {
 		if ct := content["application/json"]; ct != nil {
diff --git a/openapi3/link.go b/openapi3/link.go
index 6815fb4..2c1ec01 100644
--- a/openapi3/link.go
+++ b/openapi3/link.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
 	"github.com/getkin/kin-openapi/jsoninfo"
@@ -28,7 +29,7 @@ func (value *Link) UnmarshalJSON(data []byte) error {
 
 func (value *Link) Validate(c context.Context) error {
 	if value.OperationID == "" && value.OperationRef == "" {
-		return fmt.Errorf("missing operationId or operationRef on link")
+		return errors.New("missing operationId or operationRef on link")
 	}
 	if value.OperationID != "" && value.OperationRef != "" {
 		return fmt.Errorf("operationId '%s' and operationRef '%s' are mutually exclusive", value.OperationID, value.OperationRef)
diff --git a/openapi3/response.go b/openapi3/response.go
index dbe0972..db83db7 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"errors"
 	"strconv"
 
 	"github.com/getkin/kin-openapi/jsoninfo"
@@ -34,7 +35,7 @@ func (responses Responses) Validate(c context.Context) error {
 // Response is specified by OpenAPI/Swagger 3.0 standard.
 type Response struct {
 	ExtensionProps
-	Description string                `json:"description,omitempty" yaml:"description,omitempty"`
+	Description *string               `json:"description,omitempty" yaml:"description,omitempty"`
 	Headers     map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"`
 	Content     Content               `json:"content,omitempty" yaml:"content,omitempty"`
 	Links       map[string]*LinkRef   `json:"links,omitempty" yaml:"links,omitempty"`
@@ -45,7 +46,7 @@ func NewResponse() *Response {
 }
 
 func (response *Response) WithDescription(value string) *Response {
-	response.Description = value
+	response.Description = &value
 	return response
 }
 
@@ -73,6 +74,10 @@ func (response *Response) UnmarshalJSON(data []byte) error {
 }
 
 func (response *Response) Validate(c context.Context) error {
+	if response.Description == nil {
+		return errors.New("A short description of the response is required")
+	}
+
 	if content := response.Content; content != nil {
 		if err := content.Validate(c); err != nil {
 			return err
diff --git a/openapi3/schema.go b/openapi3/schema.go
index a285a36..f068097 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -19,6 +19,9 @@ var (
 	// SchemaErrorDetailsDisabled disables printing of details about schema errors.
 	SchemaErrorDetailsDisabled = false
 
+	//SchemaFormatValidationDisabled disables validation of schema type formats.
+	SchemaFormatValidationDisabled = false
+
 	errSchema = errors.New("Input does not match the schema")
 
 	ErrSchemaInputNaN = errors.New("NaN is not allowed")
@@ -119,9 +122,9 @@ func (schema *Schema) NewRef() *SchemaRef {
 }
 
 func NewOneOfSchema(schemas ...*Schema) *Schema {
-	refs := make([]*SchemaRef, len(schemas))
-	for i, schema := range schemas {
-		refs[i] = &SchemaRef{Value: schema}
+	refs := make([]*SchemaRef, 0, len(schemas))
+	for _, schema := range schemas {
+		refs = append(refs, &SchemaRef{Value: schema})
 	}
 	return &Schema{
 		OneOf: refs,
@@ -129,9 +132,9 @@ func NewOneOfSchema(schemas ...*Schema) *Schema {
 }
 
 func NewAnyOfSchema(schemas ...*Schema) *Schema {
-	refs := make([]*SchemaRef, len(schemas))
-	for i, schema := range schemas {
-		refs[i] = &SchemaRef{Value: schema}
+	refs := make([]*SchemaRef, 0, len(schemas))
+	for _, schema := range schemas {
+		refs = append(refs, &SchemaRef{Value: schema})
 	}
 	return &Schema{
 		AnyOf: refs,
@@ -139,9 +142,9 @@ func NewAnyOfSchema(schemas ...*Schema) *Schema {
 }
 
 func NewAllOfSchema(schemas ...*Schema) *Schema {
-	refs := make([]*SchemaRef, len(schemas))
-	for i, schema := range schemas {
-		refs[i] = &SchemaRef{Value: schema}
+	refs := make([]*SchemaRef, 0, len(schemas))
+	for _, schema := range schemas {
+		refs = append(refs, &SchemaRef{Value: schema})
 	}
 	return &Schema{
 		AllOf: refs,
@@ -436,7 +439,7 @@ func (schema *Schema) IsEmpty() bool {
 }
 
 func (schema *Schema) Validate(c context.Context) error {
-	return schema.validate(c, make([]*Schema, 2))
+	return schema.validate(c, []*Schema{})
 }
 
 func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
@@ -496,7 +499,9 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
 			switch format {
 			case "float", "double":
 			default:
-				return unsupportedFormat(format)
+				if !SchemaFormatValidationDisabled {
+					return unsupportedFormat(format)
+				}
 			}
 		}
 	case "integer":
@@ -504,7 +509,9 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
 			switch format {
 			case "int32", "int64":
 			default:
-				return unsupportedFormat(format)
+				if !SchemaFormatValidationDisabled {
+					return unsupportedFormat(format)
+				}
 			}
 		}
 	case "string":
@@ -520,7 +527,7 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
 			case "json-pointer", "relative-json-pointer":
 			default:
 				// Try to check for custom defined formats
-				if _, ok := SchemaStringFormats[format]; !ok {
+				if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled {
 					return unsupportedFormat(format)
 				}
 			}
@@ -1151,9 +1158,9 @@ func markSchemaErrorIndex(err error, index int) error {
 
 func (err *SchemaError) JSONPointer() []string {
 	reversePath := err.reversePath
-	path := make([]string, len(reversePath))
-	for i := range path {
-		path[i] = reversePath[len(path)-1-i]
+	path := append([]string(nil), reversePath...)
+	for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 {
+		path[left], path[right] = path[right], path[left]
 	}
 	return path
 }
diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go
index 84521d4..3925e0e 100644
--- a/openapi3/swagger_loader.go
+++ b/openapi3/swagger_loader.go
@@ -9,6 +9,8 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"reflect"
+	"strconv"
 	"strings"
 
 	"github.com/ghodss/yaml"
@@ -247,24 +249,82 @@ func isSingleRefElement(ref string) bool {
 	return !strings.Contains(ref, "#")
 }
 
-func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref string, prefix string, path *url.URL) (
-	components *Components,
-	id string,
+func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref string, path *url.URL) (
+	cursor interface{},
 	componentPath *url.URL,
 	err error,
 ) {
 	if swagger, ref, componentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil {
-		return nil, "", nil, err
+		return nil, nil, err
 	}
-	if !strings.HasPrefix(ref, prefix) {
-		err := fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
-		return nil, "", nil, err
+
+	parsedURL, err := url.Parse(ref)
+	if err != nil {
+		return nil, nil, fmt.Errorf("Can't parse reference: '%s': %v", ref, parsedURL)
+	}
+	fragment := parsedURL.Fragment
+	if !strings.HasPrefix(fragment, "/") {
+		err := fmt.Errorf("expected fragment prefix '#/' in URI '%s'", ref)
+		return nil, nil, err
+	}
+
+	cursor = swagger
+	for _, pathPart := range strings.Split(fragment[1:], "/") {
+
+		pathPart = strings.Replace(pathPart, "~1", "/", -1)
+		pathPart = strings.Replace(pathPart, "~0", "~", -1)
+
+		if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil {
+			return nil, nil, fmt.Errorf("Failed to resolve '%s' in fragment in URI: '%s': %v", ref, pathPart, err.Error())
+		}
+		if cursor == nil {
+			return nil, nil, failedToResolveRefFragmentPart(ref, pathPart)
+		}
 	}
-	id = ref[len(prefix):]
-	if strings.IndexByte(id, '/') >= 0 {
-		return nil, "", nil, failedToResolveRefFragmentPart(ref, id)
+
+	return cursor, componentPath, nil
+}
+
+func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, error) {
+	switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() {
+	case reflect.Map:
+		elementValue := val.MapIndex(reflect.ValueOf(fieldName))
+		if !elementValue.IsValid() {
+			return nil, fmt.Errorf("Map key not found: %v", fieldName)
+		}
+		return elementValue.Interface(), nil
+
+	case reflect.Slice:
+		i, err := strconv.ParseUint(fieldName, 10, 32)
+		if err != nil {
+			return nil, err
+		}
+		index := int(i)
+		if index >= val.Len() {
+			return nil, errors.New("slice index out of bounds")
+		}
+		return val.Index(index).Interface(), nil
+
+	case reflect.Struct:
+		for i := 0; i < val.NumField(); i++ {
+			field := val.Type().Field(i)
+			tagValue := field.Tag.Get("yaml")
+			yamlKey := strings.Split(tagValue, ",")[0]
+			if yamlKey == fieldName {
+				return val.Field(i).Interface(), nil
+			}
+		}
+		// if cursor if a "ref wrapper" struct (e.g. RequestBodyRef), try digging into its Value field
+		_, ok := val.Type().FieldByName("Value")
+		if ok {
+			return drillIntoSwaggerField(val.FieldByName("Value").Interface(), fieldName) // recurse into .Value
+		}
+		// give up
+		return nil, fmt.Errorf("Struct field not found: %v", fieldName)
+
+	default:
+		return nil, errors.New("not a map, slice nor struct")
 	}
-	return &swagger.Components, id, componentPath, nil
 }
 
 func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref string, path *url.URL) (*Swagger, string, *url.URL, error) {
@@ -313,16 +373,12 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component
 
 			component.Value = &header
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
 			if err != nil {
 				return err
 			}
-			definitions := components.Headers
-			if definitions == nil {
-				return failedToResolveRefFragment(ref)
-			}
-			resolved := definitions[id]
-			if resolved == nil {
+			resolved, ok := untypedResolved.(*HeaderRef)
+			if !ok {
 				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveHeaderRef(swagger, resolved, componentPath); err != nil {
@@ -362,17 +418,13 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon
 			}
 			component.Value = &param
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, documentPath)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath)
 			if err != nil {
 				return err
 			}
-			definitions := components.Parameters
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "parameters")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			resolved, ok := untypedResolved.(*ParameterRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveParameterRef(swagger, resolved, componentPath); err != nil {
 				return err
@@ -427,17 +479,13 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp
 
 			component.Value = &requestBody
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
 			if err != nil {
 				return err
 			}
-			definitions := components.RequestBodies
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "requestBodies")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			resolved, ok := untypedResolved.(*RequestBodyRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err = swaggerLoader.resolveRequestBodyRef(swagger, resolved, componentPath); err != nil {
 				return err
@@ -486,17 +534,13 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone
 
 			component.Value = &resp
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, documentPath)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath)
 			if err != nil {
 				return err
 			}
-			definitions := components.Responses
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "responses")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			resolved, ok := untypedResolved.(*ResponseRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveResponseRef(swagger, resolved, componentPath); err != nil {
 				return err
@@ -562,17 +606,14 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component
 			}
 			component.Value = &schema
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, documentPath)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath)
 			if err != nil {
 				return err
 			}
-			definitions := components.Schemas
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "schemas")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+
+			resolved, ok := untypedResolved.(*SchemaRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveSchemaRef(swagger, resolved, componentPath); err != nil {
 				return err
@@ -650,17 +691,13 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c
 
 			component.Value = &scheme
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
 			if err != nil {
 				return err
 			}
-			definitions := components.SecuritySchemes
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "securitySchemes")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			resolved, ok := untypedResolved.(*SecuritySchemeRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveSecuritySchemeRef(swagger, resolved, componentPath); err != nil {
 				return err
@@ -689,17 +726,13 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen
 
 			component.Value = &example
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
 			if err != nil {
 				return err
 			}
-			definitions := components.Examples
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "examples")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			resolved, ok := untypedResolved.(*ExampleRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveExampleRef(swagger, resolved, componentPath); err != nil {
 				return err
@@ -728,17 +761,13 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *
 
 			component.Value = &link
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
 			if err != nil {
 				return err
 			}
-			definitions := components.Links
-			if definitions == nil {
-				return failedToResolveRefFragmentPart(ref, "links")
-			}
-			resolved := definitions[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			resolved, ok := untypedResolved.(*LinkRef)
+			if !ok {
+				return failedToResolveRefFragment(ref)
 			}
 			if err := swaggerLoader.resolveLinkRef(swagger, resolved, componentPath); err != nil {
 				return err
diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json
new file mode 100644
index 0000000..00f4a92
--- /dev/null
+++ b/openapi3filter/fixtures/petstore.json
@@ -0,0 +1,1200 @@
+{
+  "openapi": "3.0.0",
+  "servers": [
+    {
+      "url": "https://petstore.swagger.io/v2"
+    },
+    {
+      "url": "http://petstore.swagger.io/v2"
+    }
+  ],
+  "info": {
+    "description": "This is a sample server Petstore server.  You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).  For this sample, you can use the api key `special-key` to test the authorization filters.",
+    "version": "1.0.0",
+    "title": "Swagger Petstore",
+    "termsOfService": "http://swagger.io/terms/",
+    "contact": {
+      "email": "apiteam@swagger.io"
+    },
+    "license": {
+      "name": "Apache 2.0",
+      "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+    }
+  },
+  "tags": [
+    {
+      "name": "pet",
+      "description": "Everything about your Pets",
+      "externalDocs": {
+        "description": "Find out more",
+        "url": "http://swagger.io"
+      }
+    },
+    {
+      "name": "store",
+      "description": "Access to Petstore orders"
+    },
+    {
+      "name": "user",
+      "description": "Operations about user",
+      "externalDocs": {
+        "description": "Find out more about our store",
+        "url": "http://swagger.io"
+      }
+    }
+  ],
+  "paths": {
+    "/pet": {
+      "post": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Add a new pet to the store",
+        "description": "",
+        "operationId": "addPet",
+        "responses": {
+          "405": {
+            "description": "Invalid input"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "requestBody": {
+          "$ref": "#/components/requestBodies/Pet"
+        }
+      },
+      "put": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Update an existing pet",
+        "description": "",
+        "operationId": "updatePet",
+        "responses": {
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Pet not found"
+          },
+          "405": {
+            "description": "Validation exception"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "requestBody": {
+          "$ref": "#/components/requestBodies/Pet"
+        }
+      }
+    },
+    "/pet/findByStatus": {
+      "get": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Finds Pets by status",
+        "description": "Multiple status values can be provided with comma separated strings",
+        "operationId": "findPetsByStatus",
+        "parameters": [
+          {
+            "name": "status",
+            "in": "query",
+            "description": "Status values that need to be considered for filter",
+            "required": true,
+            "explode": true,
+            "schema": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "enum": [
+                  "available",
+                  "pending",
+                  "sold"
+                ],
+                "default": "available"
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid status value"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ]
+      }
+    },
+    "/pet/findByTags": {
+      "get": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Finds Pets by tags",
+        "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+        "operationId": "findPetsByTags",
+        "parameters": [
+          {
+            "name": "tags",
+            "in": "query",
+            "description": "Tags to filter by",
+            "required": true,
+            "explode": true,
+            "schema": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid tag value"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "deprecated": true
+      }
+    },
+    "/pet/findByIds": {
+      "get": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Finds Pets by IDs",
+        "description": "Muliple IDs can be provided with comma separated strings. Use id1, id2, id3 for testing.",
+        "operationId": "findPetsByIds",
+        "parameters": [
+          {
+            "name": "ids",
+            "in": "query",
+            "description": "IDs to filter by",
+            "required": true,
+            "explode": false,
+            "schema": {
+              "type": "array",
+              "items": {
+                "type": "integer",
+                "format": "int64"
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid ID value"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "deprecated": true
+      }
+    },
+    "/pet/findByKind": {
+      "get": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Finds Pets by Kind",
+        "description": "Muliple kinds can be provided with comma separated strings. Use id1, id2, id3 for testing.",
+        "operationId": "findPetsByKind",
+        "parameters": [
+          {
+            "name": "kind",
+            "in": "query",
+            "description": "Kinds to filter by",
+            "required": true,
+            "explode": false,
+            "style": "pipeDelimited",
+            "schema": {
+              "type": "array",
+              "items": {
+                "type": "string",
+                "enum": [
+                  "dog",
+                  "cat",
+                  "turtle",
+                  "bird,with,commas"
+                ]
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid ID value"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "deprecated": true
+      }
+    },
+    "/pet/{petId}": {
+      "get": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Find pet by ID",
+        "description": "Returns a single pet",
+        "operationId": "getPetById",
+        "parameters": [
+          {
+            "name": "petId",
+            "in": "path",
+            "description": "ID of pet to return",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/Pet"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Pet"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Pet not found"
+          }
+        },
+        "security": [
+          {
+            "api_key": []
+          }
+        ]
+      },
+      "post": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Updates a pet in the store with form data",
+        "description": "",
+        "operationId": "updatePetWithForm",
+        "parameters": [
+          {
+            "name": "petId",
+            "in": "path",
+            "description": "ID of pet that needs to be updated",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          }
+        ],
+        "responses": {
+          "405": {
+            "description": "Invalid input"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/x-www-form-urlencoded": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "name": {
+                    "description": "Updated name of the pet",
+                    "type": "string"
+                  },
+                  "status": {
+                    "description": "Updated status of the pet",
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "Deletes a pet",
+        "description": "",
+        "operationId": "deletePet",
+        "parameters": [
+          {
+            "name": "api_key",
+            "in": "header",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "petId",
+            "in": "path",
+            "description": "Pet id to delete",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          }
+        ],
+        "responses": {
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Pet not found"
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ]
+      }
+    },
+    "/pet/{petId}/uploadImage": {
+      "post": {
+        "tags": [
+          "pet"
+        ],
+        "summary": "uploads an image",
+        "description": "",
+        "operationId": "uploadFile",
+        "parameters": [
+          {
+            "name": "petId",
+            "in": "path",
+            "description": "ID of pet to update",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ApiResponse"
+                }
+              }
+            }
+          }
+        },
+        "security": [
+          {
+            "petstore_auth": [
+              "write:pets",
+              "read:pets"
+            ]
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/octet-stream": {
+              "schema": {
+                "type": "string",
+                "format": "binary"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/store/inventory": {
+      "get": {
+        "tags": [
+          "store"
+        ],
+        "summary": "Returns pet inventories by status",
+        "description": "Returns a map of status codes to quantities",
+        "operationId": "getInventory",
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "additionalProperties": {
+                    "type": "integer",
+                    "format": "int32"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "security": [
+          {
+            "api_key": []
+          }
+        ]
+      }
+    },
+    "/store/order": {
+      "post": {
+        "tags": [
+          "store"
+        ],
+        "summary": "Place an order for a pet",
+        "description": "",
+        "operationId": "placeOrder",
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/Order"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Order"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid Order"
+          }
+        },
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Order"
+              }
+            }
+          },
+          "description": "order placed for purchasing the pet",
+          "required": true
+        }
+      }
+    },
+    "/store/order/{orderId}": {
+      "get": {
+        "tags": [
+          "store"
+        ],
+        "summary": "Find purchase order by ID",
+        "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions",
+        "operationId": "getOrderById",
+        "parameters": [
+          {
+            "name": "orderId",
+            "in": "path",
+            "description": "ID of pet that needs to be fetched",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int64",
+              "minimum": 1,
+              "maximum": 10
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/Order"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Order"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Order not found"
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "store"
+        ],
+        "summary": "Delete purchase order by ID",
+        "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors",
+        "operationId": "deleteOrder",
+        "parameters": [
+          {
+            "name": "orderId",
+            "in": "path",
+            "description": "ID of the order that needs to be deleted",
+            "required": true,
+            "schema": {
+              "type": "integer",
+              "format": "int64",
+              "minimum": 1
+            }
+          }
+        ],
+        "responses": {
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Order not found"
+          }
+        }
+      }
+    },
+    "/user": {
+      "post": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Create user",
+        "description": "This can only be done by the logged in user.",
+        "operationId": "createUser",
+        "responses": {
+          "default": {
+            "description": "successful operation"
+          }
+        },
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/User"
+              }
+            }
+          },
+          "description": "Created user object",
+          "required": true
+        }
+      }
+    },
+    "/user/createWithArray": {
+      "post": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Creates list of users with given input array",
+        "description": "",
+        "operationId": "createUsersWithArrayInput",
+        "responses": {
+          "default": {
+            "description": "successful operation"
+          }
+        },
+        "requestBody": {
+          "$ref": "#/components/requestBodies/UserArray"
+        }
+      }
+    },
+    "/user/createWithList": {
+      "post": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Creates list of users with given input array",
+        "description": "",
+        "operationId": "createUsersWithListInput",
+        "responses": {
+          "default": {
+            "description": "successful operation"
+          }
+        },
+        "requestBody": {
+          "$ref": "#/components/requestBodies/UserArray"
+        }
+      }
+    },
+    "/user/login": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Logs user into the system",
+        "description": "",
+        "operationId": "loginUser",
+        "parameters": [
+          {
+            "name": "username",
+            "in": "query",
+            "description": "The user name for login",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "password",
+            "in": "query",
+            "description": "The password for login in clear text",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "headers": {
+              "X-Rate-Limit": {
+                "description": "calls per hour allowed by the user",
+                "schema": {
+                  "type": "integer",
+                  "format": "int32"
+                }
+              },
+              "X-Expires-After": {
+                "description": "date in UTC when token expires",
+                "schema": {
+                  "type": "string",
+                  "format": "date-time"
+                }
+              }
+            },
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "type": "string"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid username/password supplied"
+          }
+        }
+      }
+    },
+    "/user/logout": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Logs out current logged in user session",
+        "description": "",
+        "operationId": "logoutUser",
+        "responses": {
+          "default": {
+            "description": "successful operation"
+          }
+        }
+      }
+    },
+    "/user/{username}": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Get user by user name",
+        "description": "",
+        "operationId": "getUserByName",
+        "parameters": [
+          {
+            "name": "username",
+            "in": "path",
+            "description": "The name that needs to be fetched. Use user1 for testing. ",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/User"
+                }
+              },
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/User"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid username supplied"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Updated user",
+        "description": "This can only be done by the logged in user.",
+        "operationId": "updateUser",
+        "parameters": [
+          {
+            "name": "username",
+            "in": "path",
+            "description": "name that need to be updated",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "400": {
+            "description": "Invalid user supplied"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        },
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/User"
+              }
+            }
+          },
+          "description": "Updated user object",
+          "required": true
+        }
+      },
+      "delete": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Delete user",
+        "description": "This can only be done by the logged in user.",
+        "operationId": "deleteUser",
+        "parameters": [
+          {
+            "name": "username",
+            "in": "path",
+            "description": "The name that needs to be deleted",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "400": {
+            "description": "Invalid username supplied"
+          },
+          "404": {
+            "description": "User not found"
+          }
+        }
+      }
+    }
+  },
+  "externalDocs": {
+    "description": "Find out more about Swagger",
+    "url": "http://swagger.io"
+  },
+  "components": {
+    "schemas": {
+      "Order": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "petId": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "quantity": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "shipDate": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "status": {
+            "type": "string",
+            "description": "Order Status",
+            "enum": [
+              "placed",
+              "approved",
+              "delivered"
+            ]
+          },
+          "complete": {
+            "type": "boolean",
+            "default": false
+          }
+        },
+        "xml": {
+          "name": "Order"
+        }
+      },
+      "User": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "username": {
+            "type": "string"
+          },
+          "firstName": {
+            "type": "string"
+          },
+          "lastName": {
+            "type": "string"
+          },
+          "email": {
+            "type": "string"
+          },
+          "password": {
+            "type": "string"
+          },
+          "phone": {
+            "type": "string"
+          },
+          "userStatus": {
+            "type": "integer",
+            "format": "int32",
+            "description": "User Status"
+          }
+        },
+        "xml": {
+          "name": "User"
+        }
+      },
+      "Category": {
+        "type": "object",
+        "required": [
+          "name"
+        ],
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "name": {
+            "type": "string"
+          },
+          "tags": {
+            "type": "array",
+            "xml": {
+              "name": "tag",
+              "wrapped": true
+            },
+            "items": {
+              "$ref": "#/components/schemas/Tag"
+            }
+          }
+        },
+        "xml": {
+          "name": "Category"
+        }
+      },
+      "Tag": {
+        "type": "object",
+        "required": [
+          "name"
+        ],
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "name": {
+            "type": "string"
+          }
+        },
+        "xml": {
+          "name": "Tag"
+        }
+      },
+      "ApiResponse": {
+        "type": "object",
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "type": {
+            "type": "string"
+          },
+          "message": {
+            "type": "string"
+          }
+        }
+      },
+      "Pet": {
+        "type": "object",
+        "required": [
+          "name",
+          "photoUrls"
+        ],
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int64",
+            "readOnly": true
+          },
+          "category": {
+            "$ref": "#/components/schemas/Category"
+          },
+          "name": {
+            "type": "string",
+            "example": "doggie"
+          },
+          "photoUrls": {
+            "type": "array",
+            "xml": {
+              "name": "photoUrl",
+              "wrapped": true
+            },
+            "items": {
+              "type": "string"
+            }
+          },
+          "tags": {
+            "type": "array",
+            "xml": {
+              "name": "tag",
+              "wrapped": true
+            },
+            "items": {
+              "$ref": "#/components/schemas/Tag"
+            }
+          },
+          "status": {
+            "type": "string",
+            "description": "pet status in the store",
+            "enum": [
+              "available",
+              "pending",
+              "sold"
+            ]
+          }
+        },
+        "xml": {
+          "name": "Pet"
+        }
+      }
+    },
+    "requestBodies": {
+      "Pet": {
+        "content": {
+          "application/json": {
+            "schema": {
+              "$ref": "#/components/schemas/Pet"
+            }
+          },
+          "application/xml": {
+            "schema": {
+              "$ref": "#/components/schemas/Pet"
+            }
+          }
+        },
+        "description": "Pet object that needs to be added to the store",
+        "required": true
+      },
+      "UserArray": {
+        "content": {
+          "application/json": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/components/schemas/User"
+              }
+            }
+          }
+        },
+        "description": "List of user object",
+        "required": true
+      }
+    },
+    "securitySchemes": {
+      "petstore_auth": {
+        "type": "oauth2",
+        "flows": {
+          "implicit": {
+            "authorizationUrl": "https://petstore.swagger.io/oauth/dialog",
+            "scopes": {
+              "write:pets": "modify pets in your account",
+              "read:pets": "read your pets"
+            }
+          }
+        }
+      },
+      "api_key": {
+        "type": "apiKey",
+        "name": "api_key",
+        "in": "header"
+      }
+    }
+  }
+}
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index 06ce223..3b1de11 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -104,16 +104,21 @@ func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error {
 func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) (
 	value interface{}, schema *openapi3.Schema, err error) {
 
-	paramValues := make([]string, 1)
+	var paramValues []string
 	var found bool
 	switch param.In {
 	case openapi3.ParameterInPath:
-		paramValues[0], found = input.PathParams[param.Name]
+		var paramValue string
+		if paramValue, found = input.PathParams[param.Name]; found {
+			paramValues = []string{paramValue}
+		}
 	case openapi3.ParameterInQuery:
 		paramValues, found = input.GetQueryParams()[param.Name]
 	case openapi3.ParameterInHeader:
-		paramValues[0] = input.Request.Header.Get(http.CanonicalHeaderKey(param.Name))
-		found = paramValues[0] != ""
+		if paramValue := input.Request.Header.Get(http.CanonicalHeaderKey(param.Name)); paramValue != "" {
+			paramValues = []string{paramValue}
+			found = true
+		}
 	case openapi3.ParameterInCookie:
 		var cookie *http.Cookie
 		if cookie, err = input.Request.Cookie(param.Name); err == http.ErrNoCookie {
@@ -121,7 +126,7 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI
 		} else if err != nil {
 			return
 		} else {
-			paramValues[0] = cookie.Value
+			paramValues = []string{cookie.Value}
 			found = true
 		}
 	default:
@@ -178,12 +183,14 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string)
 			return
 		}
 	} else {
-		outArray := make([]interface{}, len(values))
-		for i, v := range values {
-			if err = json.Unmarshal([]byte(v), &outArray[i]); err != nil {
+		outArray := make([]interface{}, 0, len(values))
+		for _, v := range values {
+			var item interface{}
+			if err = json.Unmarshal([]byte(v), &item); err != nil {
 				err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name)
 				return
 			}
+			outArray = append(outArray, item)
 		}
 		outValue = outArray
 	}
@@ -923,7 +930,29 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
 		subEncFn := func(string) *openapi3.Encoding { return enc }
 		// If the property's schema has type "array" it is means that the form contains a few parts with the same name.
 		// Every such part has a type that is defined by an items schema in the property's schema.
-		valueSchema := schema.Value.Properties[name]
+		var valueSchema *openapi3.SchemaRef
+		var exists bool
+		valueSchema, exists = schema.Value.Properties[name]
+		if !exists {
+			anyProperties := schema.Value.AdditionalPropertiesAllowed
+			if anyProperties != nil {
+				switch *anyProperties {
+				case true:
+					//additionalProperties: true
+					continue
+				default:
+					//additionalProperties: false
+					return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+				}
+			}
+			if schema.Value.AdditionalProperties == nil {
+				return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+			}
+			valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name]
+			if !exists {
+				return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+			}
+		}
 		if valueSchema.Value.Type == "array" {
 			valueSchema = valueSchema.Value.Items
 		}
@@ -938,9 +967,18 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
 		values[name] = append(values[name], value)
 	}
 
+	allTheProperties := make(map[string]*openapi3.SchemaRef)
+	for k, v := range schema.Value.Properties {
+		allTheProperties[k] = v
+	}
+	if schema.Value.AdditionalProperties != nil {
+		for k, v := range schema.Value.AdditionalProperties.Value.Properties {
+			allTheProperties[k] = v
+		}
+	}
 	// Make an object value from form values.
 	obj := make(map[string]interface{})
-	for name, prop := range schema.Value.Properties {
+	for name, prop := range allTheProperties {
 		vv := values[name]
 		if len(vv) == 0 {
 			continue
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index 4f4b15b..f22e71e 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -8,7 +8,6 @@ import (
 	"io/ioutil"
 	"net/http"
 	"sort"
-	"sync"
 
 	"github.com/getkin/kin-openapi/openapi3"
 )
@@ -204,57 +203,23 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque
 	return nil
 }
 
-// ValidateSecurityRequirements validates a multiple OpenAPI 3 security requirements.
-// Returns nil if one of them inputed.
-// Otherwise returns an error describing the security failures.
+// ValidateSecurityRequirements goes through multiple OpenAPI 3 security
+// requirements in order and returns nil on the first valid requirement.
+// If no requirement is met, errors are returned in order.
 func ValidateSecurityRequirements(c context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error {
-	// Alternative requirements
 	if len(srs) == 0 {
 		return nil
 	}
-
-	var wg sync.WaitGroup
-	errs := make([]error, len(srs))
-
-	// For each alternative security requirement
-	for i, securityRequirement := range srs {
-		// Capture index from iteration variable
-		currentIndex := i
-		currentSecurityRequirement := securityRequirement
-
-		// Add a work item
-		wg.Add(1)
-
-		go func() {
-			defer func() {
-				v := recover()
-				if v != nil {
-					if err, ok := v.(error); ok {
-						errs[currentIndex] = err
-					} else {
-						errs[currentIndex] = errors.New("Panicked")
-					}
-				}
-
-				// Remove a work item
-				wg.Done()
-			}()
-
-			if err := validateSecurityRequirement(c, input, currentSecurityRequirement); err != nil {
-				errs[currentIndex] = err
+	var errs []error
+	for _, sr := range srs {
+		if err := validateSecurityRequirement(c, input, sr); err != nil {
+			if len(errs) == 0 {
+				errs = make([]error, 0, len(srs))
 			}
-		}()
-	}
-
-	// Wait for all
-	wg.Wait()
-
-	// If any security requirement was met
-	for _, err := range errs {
-		if err == nil {
-			// Return no error
-			return nil
+			errs = append(errs, err)
+			continue
 		}
+		return nil
 	}
 	return &SecurityRequirementsError{
 		SecurityRequirements: srs,
diff --git a/openapi3filter/validation_error.go b/openapi3filter/validation_error.go
new file mode 100644
index 0000000..bfeeaa7
--- /dev/null
+++ b/openapi3filter/validation_error.go
@@ -0,0 +1,85 @@
+package openapi3filter
+
+import (
+	"bytes"
+	"strconv"
+)
+
+// ValidationError struct provides granular error information
+// useful for communicating issues back to end user and developer.
+// Based on https://jsonapi.org/format/#error-objects
+type ValidationError struct {
+	// A unique identifier for this particular occurrence of the problem.
+	Id string `json:"id,omitempty"`
+	// The HTTP status code applicable to this problem.
+	Status int `json:"status,omitempty"`
+	// An application-specific error code, expressed as a string value.
+	Code string `json:"code,omitempty"`
+	// A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.
+	Title string `json:"title,omitempty"`
+	// A human-readable explanation specific to this occurrence of the problem.
+	Detail string `json:"detail,omitempty"`
+	// An object containing references to the source of the error
+	Source *ValidationErrorSource `json:"source,omitempty"`
+}
+
+// ValidationErrorSource struct
+type ValidationErrorSource struct {
+	// A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].
+	Pointer string `json:"pointer,omitempty"`
+	// A string indicating which query parameter caused the error.
+	Parameter string `json:"parameter,omitempty"`
+}
+
+var _ error = &ValidationError{}
+
+// Error implements the error interface.
+func (e *ValidationError) Error() string {
+	b := new(bytes.Buffer)
+	b.WriteString("[")
+	if e.Status != 0 {
+		b.WriteString(strconv.Itoa(e.Status))
+	}
+	b.WriteString("]")
+	b.WriteString("[")
+	if e.Code != "" {
+		b.WriteString(e.Code)
+	}
+	b.WriteString("]")
+	b.WriteString("[")
+	if e.Id != "" {
+		b.WriteString(e.Id)
+	}
+	b.WriteString("]")
+	b.WriteString(" ")
+	if e.Title != "" {
+		b.WriteString(e.Title)
+		b.WriteString(" ")
+	}
+	if e.Detail != "" {
+		b.WriteString("| ")
+		b.WriteString(e.Detail)
+		b.WriteString(" ")
+	}
+	if e.Source != nil {
+		b.WriteString("[source ")
+		if e.Source.Parameter != "" {
+			b.WriteString("parameter=")
+			b.WriteString(e.Source.Parameter)
+		} else if e.Source.Pointer != "" {
+			b.WriteString("pointer=")
+			b.WriteString(e.Source.Pointer)
+		}
+		b.WriteString("]")
+	}
+
+	if b.Len() == 0 {
+		return "no error"
+	}
+	return b.String()
+}
+
+// StatusCode implements the StatusCoder interface for DefaultErrorEncoder
+func (e *ValidationError) StatusCode() int {
+	return e.Status
+}
diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go
new file mode 100644
index 0000000..556bcc8
--- /dev/null
+++ b/openapi3filter/validation_error_encoder.go
@@ -0,0 +1,193 @@
+package openapi3filter
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+// ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors
+type ValidationErrorEncoder struct {
+	Encoder ErrorEncoder
+}
+
+// Encode implements the ErrorEncoder interface for encoding ValidationErrors
+func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) {
+	var cErr *ValidationError
+
+	if e, ok := err.(*RouteError); ok {
+		cErr = convertRouteError(e)
+		enc.Encoder(ctx, cErr, w)
+		return
+	}
+
+	e, ok := err.(*RequestError)
+	if !ok {
+		enc.Encoder(ctx, err, w)
+		return
+	}
+
+	if e.Err == nil {
+		cErr = convertBasicRequestError(e)
+	} else if e.Err == ErrInvalidRequired {
+		cErr = convertErrInvalidRequired(e)
+	} else if innerErr, ok := e.Err.(*ParseError); ok {
+		cErr = convertParseError(e, innerErr)
+	} else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok {
+		cErr = convertSchemaError(e, innerErr)
+	}
+
+	if cErr != nil {
+		enc.Encoder(ctx, cErr, w)
+	} else {
+		enc.Encoder(ctx, err, w)
+	}
+}
+
+func convertRouteError(e *RouteError) *ValidationError {
+	var cErr *ValidationError
+	switch e.Reason {
+	case "Path doesn't support the HTTP method":
+		cErr = &ValidationError{Status: http.StatusMethodNotAllowed, Title: e.Reason}
+	default:
+		cErr = &ValidationError{Status: http.StatusNotFound, Title: e.Reason}
+	}
+	return cErr
+}
+
+func convertBasicRequestError(e *RequestError) *ValidationError {
+	var cErr *ValidationError
+	unsupportedContentType := "header 'Content-Type' has unexpected value: "
+	if strings.HasPrefix(e.Reason, unsupportedContentType) {
+		if strings.HasSuffix(e.Reason, `: ""`) {
+			cErr = &ValidationError{
+				Status: http.StatusUnsupportedMediaType,
+				Title:  "header 'Content-Type' is required",
+			}
+		} else {
+			cErr = &ValidationError{
+				Status: http.StatusUnsupportedMediaType,
+				Title:  "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentType),
+			}
+		}
+	} else {
+		cErr = &ValidationError{
+			Status: http.StatusBadRequest,
+			Title:  e.Error(),
+		}
+	}
+	return cErr
+}
+
+func convertErrInvalidRequired(e *RequestError) *ValidationError {
+	var cErr *ValidationError
+	if e.Reason == ErrInvalidRequired.Error() && e.Parameter != nil {
+		cErr = &ValidationError{
+			Status: http.StatusBadRequest,
+			Title:  fmt.Sprintf("Parameter '%s' in %s is required", e.Parameter.Name, e.Parameter.In),
+		}
+	} else {
+		cErr = &ValidationError{
+			Status: http.StatusBadRequest,
+			Title:  e.Error(),
+		}
+	}
+	return cErr
+}
+
+func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError {
+	var cErr *ValidationError
+	// We treat path params of the wrong type like a 404 instead of a 400
+	if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" {
+		cErr = &ValidationError{
+			Status: http.StatusNotFound,
+			Title:  fmt.Sprintf("Resource not found with '%s' value: %v", e.Parameter.Name, innerErr.Value),
+		}
+	} else if strings.HasPrefix(innerErr.Reason, "unsupported content type") {
+		cErr = &ValidationError{
+			Status: http.StatusUnsupportedMediaType,
+			Title:  innerErr.Reason,
+		}
+	} else if innerErr.RootCause() != nil {
+		if rootErr, ok := innerErr.Cause.(*ParseError); ok &&
+			rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" {
+			cErr = &ValidationError{
+				Status: http.StatusBadRequest,
+				Title: fmt.Sprintf("Parameter '%s' in %s is invalid: %v is %s",
+					e.Parameter.Name, e.Parameter.In, rootErr.Value, rootErr.Reason),
+			}
+		} else {
+			cErr = &ValidationError{
+				Status: http.StatusBadRequest,
+				Title:  innerErr.Reason,
+			}
+		}
+	}
+	return cErr
+}
+
+var propertyMissingNameRE = regexp.MustCompile(`Property '(?P<name>[^']*)' is missing`)
+
+func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError {
+	cErr := &ValidationError{Title: innerErr.Reason}
+
+	// Add http status code
+	if e.Parameter != nil {
+		cErr.Status = http.StatusBadRequest
+	} else if e.RequestBody != nil {
+		cErr.Status = http.StatusUnprocessableEntity
+	}
+
+	// Add error source
+	if e.Parameter != nil && e.Parameter.In == "query" {
+		// We have a JSONPointer in the query param too so need to
+		// make sure 'Parameter' check takes priority over 'Pointer'
+		cErr.Source = &ValidationErrorSource{
+			Parameter: e.Parameter.Name,
+		}
+	} else if innerErr.JSONPointer() != nil {
+		pointer := innerErr.JSONPointer()
+
+		// JSONPointer is rarely what you expect.
+		// 1. for "property is missing" errors, its an empty array
+		// 2. for nested attributes, it leaves off the last element from the JSONPointer
+		matches := propertyMissingNameRE.FindStringSubmatch(innerErr.Reason)
+		if matches != nil && len(matches) > 1 {
+			if len(pointer) == 0 || matches[1] != pointer[0] {
+				pointer = append(pointer, matches[1])
+			}
+		}
+
+		cErr.Source = &ValidationErrorSource{
+			Pointer: toJSONPointer(pointer),
+		}
+	}
+
+	// Add details on allowed values for enums
+	if innerErr.SchemaField == "enum" &&
+		innerErr.Reason == "JSON value is not one of the allowed values" {
+		enums := make([]string, 0, len(innerErr.Schema.Enum))
+		for _, enum := range innerErr.Schema.Enum {
+			enums = append(enums, fmt.Sprintf("%v", enum))
+		}
+		cErr.Detail = fmt.Sprintf("Value '%v' at %s must be one of: %s",
+			innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", "))
+		value := fmt.Sprintf("%v", innerErr.Value)
+		if (e.Parameter.Explode == nil || *e.Parameter.Explode == true) &&
+			(e.Parameter.Style == "" || e.Parameter.Style == "form") &&
+			strings.Contains(value, ",") {
+			parts := strings.Split(value, ",")
+			cErr.Detail = cErr.Detail+"; "+ fmt.Sprintf("perhaps you intended '?%s=%s'",
+				e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"="))
+		}
+	}
+	return cErr
+}
+
+func toJSONPointer(reversePath []string) string {
+	return "/" + strings.Join(reversePath, "/")
+}
diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go
new file mode 100644
index 0000000..f72ef1c
--- /dev/null
+++ b/openapi3filter/validation_handler.go
@@ -0,0 +1,78 @@
+package openapi3filter
+
+import (
+	"context"
+	"net/http"
+)
+
+type AuthenticationFunc func(context.Context, *AuthenticationInput) error
+
+func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil }
+
+var _ AuthenticationFunc = NoopAuthenticationFunc
+
+type ValidationHandler struct {
+	Handler            http.Handler
+	AuthenticationFunc AuthenticationFunc
+	SwaggerFile        string
+	ErrorEncoder       ErrorEncoder
+	router             *Router
+}
+
+func (h *ValidationHandler) Load() error {
+	h.router = NewRouter()
+
+	err := h.router.AddSwaggerFromFile(h.SwaggerFile)
+	if err != nil {
+		return err
+	}
+
+	// set defaults
+	if h.Handler == nil {
+		h.Handler = http.DefaultServeMux
+	}
+	if h.AuthenticationFunc == nil {
+		h.AuthenticationFunc = NoopAuthenticationFunc
+	}
+	if h.ErrorEncoder == nil {
+		h.ErrorEncoder = DefaultErrorEncoder
+	}
+
+	return nil
+}
+
+func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	err := h.validateRequest(r)
+	if err != nil {
+		h.ErrorEncoder(r.Context(), err, w)
+		return
+	}
+	// TODO: validateResponse
+	h.Handler.ServeHTTP(w, r)
+}
+
+func (h *ValidationHandler) validateRequest(r *http.Request) error {
+	// Find route
+	route, pathParams, err := h.router.FindRoute(r.Method, r.URL)
+	if err != nil {
+		return err
+	}
+
+	options := &Options{
+		AuthenticationFunc: h.AuthenticationFunc,
+	}
+
+	// Validate request
+	requestValidationInput := &RequestValidationInput{
+		Request:    r,
+		PathParams: pathParams,
+		Route:      route,
+		Options:    options,
+	}
+	err = ValidateRequest(r.Context(), requestValidationInput)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/openapi3filter/validation_kit.go b/openapi3filter/validation_kit.go
new file mode 100644
index 0000000..9e11e4f
--- /dev/null
+++ b/openapi3filter/validation_kit.go
@@ -0,0 +1,85 @@
+package openapi3filter
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+)
+
+///////////////////////////////////////////////////////////////////////////////////
+// We didn't want to tie kin-openapi too tightly with go-kit.
+// This file contains the ErrorEncoder and DefaultErrorEncoder function
+// borrowed from this project.
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2015 Peter Bourgon
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+///////////////////////////////////////////////////////////////////////////////////
+
+// ErrorEncoder is responsible for encoding an error to the ResponseWriter.
+// Users are encouraged to use custom ErrorEncoders to encode HTTP errors to
+// their clients, and will likely want to pass and check for their own error
+// types. See the example shipping/handling service.
+type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)
+
+// StatusCoder is checked by DefaultErrorEncoder. If an error value implements
+// StatusCoder, the StatusCode will be used when encoding the error. By default,
+// StatusInternalServerError (500) is used.
+type StatusCoder interface {
+	StatusCode() int
+}
+
+// Headerer is checked by DefaultErrorEncoder. If an error value implements
+// Headerer, the provided headers will be applied to the response writer, after
+// the Content-Type is set.
+type Headerer interface {
+	Headers() http.Header
+}
+
+// DefaultErrorEncoder writes the error to the ResponseWriter, by default a
+// content type of text/plain, a body of the plain text of the error, and a
+// status code of 500. If the error implements Headerer, the provided headers
+// will be applied to the response. If the error implements json.Marshaler, and
+// the marshaling succeeds, a content type of application/json and the JSON
+// encoded form of the error will be used. If the error implements StatusCoder,
+// the provided StatusCode will be used instead of 500.
+func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) {
+	contentType, body := "text/plain; charset=utf-8", []byte(err.Error())
+	if marshaler, ok := err.(json.Marshaler); ok {
+		if jsonBody, marshalErr := marshaler.MarshalJSON(); marshalErr == nil {
+			contentType, body = "application/json; charset=utf-8", jsonBody
+		}
+	}
+	w.Header().Set("Content-Type", contentType)
+	if headerer, ok := err.(Headerer); ok {
+		for k, values := range headerer.Headers() {
+			for _, v := range values {
+				w.Header().Add(k, v)
+			}
+		}
+	}
+	code := http.StatusInternalServerError
+	if sc, ok := err.(StatusCoder); ok {
+		code = sc.StatusCode()
+	}
+	w.WriteHeader(code)
+	w.Write(body)
+}


Test patch:
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index ff7522f..2eeefec 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -1,11 +1,10 @@
-package openapi2conv_test
+package openapi2conv
 
 import (
 	"encoding/json"
 	"testing"
 
 	"github.com/getkin/kin-openapi/openapi2"
-	"github.com/getkin/kin-openapi/openapi2conv"
 	"github.com/getkin/kin-openapi/openapi3"
 	"github.com/stretchr/testify/require"
 )
@@ -15,7 +14,7 @@ func TestConvOpenAPIV3ToV2(t *testing.T) {
 	err := json.Unmarshal([]byte(exampleV3), &swagger3)
 	require.NoError(t, err)
 
-	actualV2, err := openapi2conv.FromV3Swagger(&swagger3)
+	actualV2, err := FromV3Swagger(&swagger3)
 	require.NoError(t, err)
 	data, err := json.Marshal(actualV2)
 	require.NoError(t, err)
@@ -27,7 +26,7 @@ func TestConvOpenAPIV2ToV3(t *testing.T) {
 	err := json.Unmarshal([]byte(exampleV2), &swagger2)
 	require.NoError(t, err)
 
-	actualV3, err := openapi2conv.ToV3Swagger(&swagger2)
+	actualV3, err := ToV3Swagger(&swagger2)
 	require.NoError(t, err)
 	data, err := json.Marshal(actualV3)
 	require.NoError(t, err)
@@ -47,6 +46,19 @@ const exampleV2 = `
     }
   ],
   "paths": {
+    "/another/{banana}/{id}": {
+        "parameters": [
+		  {
+            "$ref": "#/parameters/banana"
+          },
+          {
+            "in": "path",
+            "name": "id",
+			"type": "integer",
+			"required": true
+          }
+		]
+    },
     "/example": {
       "delete": {
         "description": "example delete",
@@ -185,6 +197,12 @@ const exampleV2 = `
       }
     }
   },
+  "parameters": {
+    "banana": {
+	  "in": "path",
+      "type": "string"
+    }
+  },
   "security": [
     {
       "default_security_0": [
@@ -214,6 +232,14 @@ const exampleV3 = `
         "description": "Insufficient permission to perform the requested action."
       }
     },
+    "parameters": {
+      "banana": {
+	    "in": "path",
+        "schema": {
+          "type": "string"
+        }
+      }
+    },
     "schemas": {
       "Item": {
         "type": "object",
@@ -255,6 +281,21 @@ const exampleV3 = `
     }
   ],
   "paths": {
+    "/another/{banana}/{id}": {
+        "parameters": [
+		  {
+            "$ref": "#/components/parameters/banana"
+          },
+          {
+            "in": "path",
+            "name": "id",
+            "schema": {
+              "type": "integer"
+            },
+			"required": true
+          }
+		]
+    },
     "/example": {
       "delete": {
         "description": "example delete",
diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go
index 43a5dfe..22f325e 100644
--- a/openapi3/operation_test.go
+++ b/openapi3/operation_test.go
@@ -40,7 +40,7 @@ func operationWithoutResponses() *Operation {
 
 func operationWithResponses() *Operation {
 	initOperation()
-	operation.AddResponse(200, NewResponse())
+	operation.AddResponse(200, NewResponse().WithDescription("some response"))
 	return operation
 }
 
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index 3c16101..e82aba2 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -319,16 +319,16 @@ var schemaExamples = []schemaExample{
 		AllValid: []interface{}{
 			"",
 			base64.StdEncoding.EncodeToString(func() []byte {
-				data := make([]byte, 1024)
-				for i := range data {
-					data[i] = byte(i)
+				data := make([]byte, 0, 1024)
+				for i := 0; i < cap(data); i++ {
+					data = append(data, byte(i))
 				}
 				return data
 			}()),
 			base64.URLEncoding.EncodeToString(func() []byte {
-				data := make([]byte, 1024)
-				for i := range data {
-					data[i] = byte(i)
+				data := make([]byte, 0, 1024)
+				for i := 0; i < cap(data); i++ {
+					data = append(data, byte(i))
 				}
 				return data
 			}()),
diff --git a/openapi3/swagger_loader_empty_response_description_test.go b/openapi3/swagger_loader_empty_response_description_test.go
new file mode 100644
index 0000000..5199ac1
--- /dev/null
+++ b/openapi3/swagger_loader_empty_response_description_test.go
@@ -0,0 +1,84 @@
+package openapi3_test
+
+import (
+	"encoding/json"
+	"strings"
+	"testing"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/stretchr/testify/require"
+)
+
+func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) {
+	const spec = `
+{
+  "info": {
+    "description": "A sample API to illustrate OpenAPI concepts",
+    "title": "Sample API",
+    "version": "1.0.0"
+  },
+  "openapi": "3.0.0",
+  "paths": {
+    "/path1": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": ""
+          }
+        }
+      }
+    }
+  }
+}
+`
+
+	{
+		spec := []byte(spec)
+		loader := openapi3.NewSwaggerLoader()
+		doc, err := loader.LoadSwaggerFromData(spec)
+		require.NoError(t, err)
+		got := doc.Paths["/path1"].Get.Responses["200"].Value.Description
+		expected := ""
+		require.Equal(t, &expected, got)
+		t.Log("Empty description provided: valid spec")
+		err = doc.Validate(loader.Context)
+		require.NoError(t, err)
+	}
+
+	{
+		spec := []byte(strings.Replace(spec, `"description": ""`, `"description": "My response"`, 1))
+		loader := openapi3.NewSwaggerLoader()
+		doc, err := loader.LoadSwaggerFromData(spec)
+		require.NoError(t, err)
+		got := doc.Paths["/path1"].Get.Responses["200"].Value.Description
+		expected := "My response"
+		require.Equal(t, &expected, got)
+		t.Log("Non-empty description provided: valid spec")
+		err = doc.Validate(loader.Context)
+		require.NoError(t, err)
+	}
+
+	noDescriptionIsInvalid := func(data []byte) *openapi3.Swagger {
+		loader := openapi3.NewSwaggerLoader()
+		doc, err := loader.LoadSwaggerFromData(data)
+		require.NoError(t, err)
+		got := doc.Paths["/path1"].Get.Responses["200"].Value.Description
+		require.Nil(t, got)
+		t.Log("No description provided: invalid spec")
+		err = doc.Validate(loader.Context)
+		require.Error(t, err)
+		return doc
+	}
+
+	var docWithNoResponseDescription *openapi3.Swagger
+	{
+		spec := []byte(strings.Replace(spec, `"description": ""`, ``, 1))
+		docWithNoResponseDescription = noDescriptionIsInvalid(spec)
+	}
+
+	str, err := json.Marshal(docWithNoResponseDescription)
+	require.NoError(t, err)
+	require.NotEmpty(t, str)
+	t.Log("Reserialization does not set description")
+	_ = noDescriptionIsInvalid(str)
+}
diff --git a/openapi3/swagger_loader_issue212_test.go b/openapi3/swagger_loader_issue212_test.go
new file mode 100644
index 0000000..1999db4
--- /dev/null
+++ b/openapi3/swagger_loader_issue212_test.go
@@ -0,0 +1,95 @@
+package openapi3
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIssue212(t *testing.T) {
+	spec := `
+openapi: 3.0.1
+info:
+  title: 'test'
+  version: 1.0.0
+servers:
+  - url: /api
+
+paths:
+  /available-products:
+    get:
+      operationId: getAvailableProductCollection
+      responses:
+        "200":
+          description: test
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: "#/components/schemas/AvailableProduct"
+
+components:
+  schemas:
+    AvailableProduct:
+      type: object
+      properties:
+        id:
+          type: string
+        type:
+          type: string
+        name:
+          type: string
+        media:
+          type: object
+          properties:
+            documents:
+              type: array
+              items:
+                allOf:
+                  - $ref: "#/components/schemas/AvailableProduct/properties/previewImage/allOf/0"
+                  - type: object
+                    properties:
+                      uri:
+                        type: string
+                        pattern: ^\/documents\/[0-9a-f]{64}$
+        previewImage:
+          allOf:
+            - type: object
+              required:
+                - id
+                - uri
+              properties:
+                id:
+                  type: string
+                uri:
+                  type: string
+            - type: object
+              properties:
+                uri:
+                  type: string
+                  pattern: ^\/images\/[0-9a-f]{64}$
+`
+
+	loader := NewSwaggerLoader()
+	doc, err := loader.LoadSwaggerFromData([]byte(spec))
+	require.NoError(t, err)
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+
+	expected, err := json.Marshal(&Schema{
+		Type:     "object",
+		Required: []string{"id", "uri"},
+		Properties: map[string]*SchemaRef{
+			"id":  {Value: &Schema{Type: "string"}},
+			"uri": {Value: &Schema{Type: "string"}},
+		},
+	},
+	)
+	require.NoError(t, err)
+	got, err := json.Marshal(doc.Components.Schemas["AvailableProduct"].Value.Properties["media"].Value.Properties["documents"].Value.Items.Value.AllOf[0].Value)
+	require.NoError(t, err)
+
+	require.Equal(t, expected, got)
+}
diff --git a/openapi3/swagger_loader_paths_test.go b/openapi3/swagger_loader_paths_test.go
new file mode 100644
index 0000000..e805d3a
--- /dev/null
+++ b/openapi3/swagger_loader_paths_test.go
@@ -0,0 +1,40 @@
+package openapi3_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/stretchr/testify/require"
+)
+
+func TestPathsMustStartWithSlash(t *testing.T) {
+	spec := `
+openapi: "3.0"
+info:
+  version: "1.0"
+  title: sample
+basePath: /adc/v1
+paths:
+  PATH:
+    get:
+      responses:
+        200:
+          description: description
+`
+
+	for path, expectedErr := range map[string]string{
+		"foo/bar":  "Error when validating Paths: Path 'foo/bar' does not start with '/'",
+		"/foo/bar": "",
+	} {
+		loader := openapi3.NewSwaggerLoader()
+		doc, err := loader.LoadSwaggerFromData([]byte(strings.Replace(spec, "PATH", path, 1)))
+		require.NoError(t, err)
+		err = doc.Validate(loader.Context)
+		if expectedErr != "" {
+			require.EqualError(t, err, expectedErr)
+		} else {
+			require.NoError(t, err)
+		}
+	}
+}
diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go
index 0e4c19d..5ad7585 100644
--- a/openapi3/swagger_loader_relative_refs_test.go
+++ b/openapi3/swagger_loader_relative_refs_test.go
@@ -36,8 +36,8 @@ var refTestDataEntries = []refTestDataEntry{
 		name:            "ResponseRef",
 		contentTemplate: externalResponseRefTemplate,
 		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
-			require.NotNil(t, swagger.Components.Responses["TestResponse"].Value.Description)
-			require.Equal(t, "description", swagger.Components.Responses["TestResponse"].Value.Description)
+			desc := "description"
+			require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description)
 		},
 	},
 	{
@@ -108,7 +108,8 @@ var refTestDataEntries = []refTestDataEntry{
 		contentTemplate: externalPathOperationResponseRefTemplate,
 		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
 			require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value)
-			require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Description)
+			desc := "description"
+			require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description)
 		},
 	},
 	{
@@ -152,7 +153,8 @@ var refTestDataEntries = []refTestDataEntry{
 		contentTemplate: externalPathOperationResponseExampleRefTemplate,
 		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
 			require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value)
-			require.Equal(t, "testdescription", swagger.Paths["/test"].Post.Responses["default"].Value.Description)
+			desc := "testdescription"
+			require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description)
 			require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description)
 		},
 	},
@@ -161,7 +163,8 @@ var refTestDataEntries = []refTestDataEntry{
 		contentTemplate: externalPathOperationResponseSchemaRefTemplate,
 		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
 			require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value)
-			require.Equal(t, "testdescription", swagger.Paths["/test"].Post.Responses["default"].Value.Description)
+			desc := "testdescription"
+			require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description)
 			require.Equal(t, "string", swagger.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type)
 		},
 	},
@@ -731,8 +734,8 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{
 		name:            "ResponseRef",
 		contentTemplate: relativeResponseDocsRefTemplate,
 		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
-			require.NotNil(t, swagger.Components.Responses["TestResponse"].Value.Description)
-			require.Equal(t, "description", swagger.Components.Responses["TestResponse"].Value.Description)
+			desc := "description"
+			require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description)
 		},
 	},
 	{
diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go
index 62b0c48..920911a 100644
--- a/openapi3/swagger_loader_test.go
+++ b/openapi3/swagger_loader_test.go
@@ -5,7 +5,6 @@ import (
 	"net"
 	"net/http"
 	"net/http/httptest"
-
 	"net/url"
 	"testing"
 
@@ -62,7 +61,8 @@ paths:
 	require.Equal(t, 2, len(doc.Components.Schemas))
 	require.Equal(t, 1, len(doc.Paths))
 	def := doc.Paths["/items"].Put.Responses.Default().Value
-	require.Equal(t, "unexpected error", def.Description)
+	desc := "unexpected error"
+	require.Equal(t, &desc, def.Description)
 	err = doc.Validate(loader.Context)
 	require.NoError(t, err)
 }
@@ -332,7 +332,8 @@ func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) {
 	require.NoError(t, err)
 
 	require.NotNil(t, swagger.Components.Responses["SomeResponse"])
-	require.Equal(t, "this is a single response definition", swagger.Components.Responses["SomeResponse"].Value.Description)
+	desc := "this is a single response definition"
+	require.Equal(t, &desc, swagger.Components.Responses["SomeResponse"].Value.Description)
 }
 
 func TestLoadRequestResponseHeaderRef(t *testing.T) {
@@ -488,3 +489,65 @@ paths:
 	require.Equal(t, "getUserById", link.OperationID)
 	require.Equal(t, "link to to the father", link.Description)
 }
+
+func TestResolveNonComponentsRef(t *testing.T) {
+	spec := []byte(`
+openapi: 3.0.0
+info:
+  title: An API
+  version: v1
+
+components:
+  schemas:
+    NewItem:
+      required: [name]
+      properties:
+        name: {type: string}
+        tag: {type: string}
+    ErrorModel:
+      type: object
+      required: [code, message]
+      properties:
+        code: {type: integer}
+        message: {type: string}
+
+paths:
+  /items:
+    put:
+      description: ''
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/NewItem'
+      responses:
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorModel'
+    post:
+      description: ''
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/paths/~1items/put/requestBody/content/application~1json/schema'
+      responses:
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorModel'
+`)
+
+	loader := openapi3.NewSwaggerLoader()
+	doc, err := loader.LoadSwaggerFromData(spec)
+	require.NoError(t, err)
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+}
diff --git a/openapi3/swagger_test.go b/openapi3/swagger_test.go
index 673e5f0..d4c4339 100644
--- a/openapi3/swagger_test.go
+++ b/openapi3/swagger_test.go
@@ -250,8 +250,9 @@ func spec() *openapi3.Swagger {
 	requestBody := &openapi3.RequestBody{
 		Description: "Some request body",
 	}
+	responseDescription := "Some response"
 	response := &openapi3.Response{
-		Description: "Some response",
+		Description: &responseDescription,
 	}
 	schema := &openapi3.Schema{
 		Description: "Some schema",
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 49fb3a1..4a5cefc 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -980,6 +980,27 @@ func TestDecodeBody(t *testing.T) {
 	})
 	require.NoError(t, err)
 
+	multipartFormExtraPart, multipartFormMimeExtraPart, err := newTestMultipartForm([]*testFormPart{
+		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
+		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
+	})
+	require.NoError(t, err)
+
+	multipartAnyAdditionalProps, multipartMimeAnyAdditionalProps, err := newTestMultipartForm([]*testFormPart{
+		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
+		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
+	})
+	multipartAdditionalProps, multipartMimeAdditionalProps, err := newTestMultipartForm([]*testFormPart{
+		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
+		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
+	})
+	multipartAdditionalPropsErr, multipartMimeAdditionalPropsErr, err := newTestMultipartForm([]*testFormPart{
+		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
+		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
+		{name: "y", contentType: "text/plain", data: strings.NewReader("y1")},
+	})
+	require.NoError(t, err)
+
 	testCases := []struct {
 		name     string
 		mime     string
@@ -1060,6 +1081,45 @@ func TestDecodeBody(t *testing.T) {
 				WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")),
 			want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo"},
 		},
+		{
+			name: "multipartExtraPart",
+			mime: multipartFormMimeExtraPart,
+			body: multipartFormExtraPart,
+			schema: openapi3.NewObjectSchema().
+				WithProperty("a", openapi3.NewStringSchema()),
+			want:    map[string]interface{}{"a": "a1"},
+			wantErr: &ParseError{Kind: KindOther},
+		},
+		{
+			name: "multipartAnyAdditionalProperties",
+			mime: multipartMimeAnyAdditionalProps,
+			body: multipartAnyAdditionalProps,
+			schema: openapi3.NewObjectSchema().
+				WithAnyAdditionalProperties().
+				WithProperty("a", openapi3.NewStringSchema()),
+			want: map[string]interface{}{"a": "a1"},
+		},
+		{
+			name: "multipartWithAdditionalProperties",
+			mime: multipartMimeAdditionalProps,
+			body: multipartAdditionalProps,
+			schema: openapi3.NewObjectSchema().
+				WithAdditionalProperties(openapi3.NewObjectSchema().
+					WithProperty("x", openapi3.NewStringSchema())).
+				WithProperty("a", openapi3.NewStringSchema()),
+			want: map[string]interface{}{"a": "a1", "x": "x1"},
+		},
+		{
+			name: "multipartWithAdditionalPropertiesError",
+			mime: multipartMimeAdditionalPropsErr,
+			body: multipartAdditionalPropsErr,
+			schema: openapi3.NewObjectSchema().
+				WithAdditionalProperties(openapi3.NewObjectSchema().
+					WithProperty("x", openapi3.NewStringSchema())).
+				WithProperty("a", openapi3.NewStringSchema()),
+			want:    map[string]interface{}{"a": "a1", "x": "x1"},
+			wantErr: &ParseError{Kind: KindOther},
+		},
 		{
 			name: "file",
 			mime: "application/octet-stream",
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
new file mode 100644
index 0000000..74e5a55
--- /dev/null
+++ b/openapi3filter/validation_error_test.go
@@ -0,0 +1,570 @@
+package openapi3filter
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/stretchr/testify/require"
+)
+
+func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http.Request {
+	host := "petstore.swagger.io"
+	pathPrefix := "v2"
+	r, err := http.NewRequest(method, fmt.Sprintf("http://%s/%s%s", host, pathPrefix, path), body)
+	require.NoError(t, err)
+	r.Header.Set("Content-Type", "application/json")
+	r.Header.Set("Authorization", "Bearer magicstring")
+	r.Host = host
+	return r
+}
+
+type validationFields struct {
+	Handler      http.Handler
+	SwaggerFile  string
+	ErrorEncoder ErrorEncoder
+}
+type validationArgs struct {
+	r *http.Request
+}
+type validationTest struct {
+	name                string
+	fields              validationFields
+	args                validationArgs
+	wantErr             bool
+	wantErrBody         string
+	wantErrReason       string
+	wantErrSchemaReason string
+	wantErrSchemaPath   string
+	wantErrSchemaValue  interface{}
+	wantErrParam        string
+	wantErrParamIn      string
+	wantErrParseKind    ParseErrorKind
+	wantErrParseValue   interface{}
+	wantErrParseReason  string
+	wantErrResponse     *ValidationError
+}
+
+func getValidationTests(t *testing.T) []*validationTest {
+	badHost, _ := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil)
+	badPath := newPetstoreRequest(t, http.MethodGet, "/watdis", nil)
+	badMethod := newPetstoreRequest(t, http.MethodTrace, "/pet", nil)
+
+	missingBody1 := newPetstoreRequest(t, http.MethodPost, "/pet", nil)
+	missingBody2 := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(``))
+
+	noContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
+	noContentType.Header.Del("Content-Type")
+
+	noContentTypeNeeded := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil)
+	noContentTypeNeeded.Header.Del("Content-Type")
+
+	unknownContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
+	unknownContentType.Header.Set("Content-Type", "application/xml")
+
+	unsupportedContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
+	unsupportedContentType.Header.Set("Content-Type", "text/plain")
+
+	return []*validationTest{
+		//
+		// Basics
+		//
+
+		{
+			name: "error - unknown host",
+			args: validationArgs{
+				r: badHost,
+			},
+			wantErrReason:   "Does not match any server",
+			wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: "Does not match any server"},
+		},
+		{
+			name: "error - unknown path",
+			args: validationArgs{
+				r: badPath,
+			},
+			wantErrReason:   "Path was not found",
+			wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: "Path was not found"},
+		},
+		{
+			name: "error - unknown method",
+			args: validationArgs{
+				r: badMethod,
+			},
+			wantErrReason: "Path doesn't support the HTTP method",
+			// 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: "Path doesn't support the HTTP method"},
+		},
+		{
+			name: "error - missing body on POST",
+			args: validationArgs{
+				r: missingBody1,
+			},
+			wantErrBody: "Request body has an error: must have a value",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "Request body has an error: must have a value"},
+		},
+		{
+			name: "error - empty body on POST",
+			args: validationArgs{
+				r: missingBody2,
+			},
+			wantErrBody: "Request body has an error: must have a value",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "Request body has an error: must have a value"},
+		},
+
+		//
+		// Content-Type
+		//
+
+		{
+			name: "error - missing content-type on POST",
+			args: validationArgs{
+				r: noContentType,
+			},
+			wantErrReason: "header 'Content-Type' has unexpected value: \"\"",
+			wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
+				Title: "header 'Content-Type' is required"},
+		},
+		{
+			name: "error - unknown content-type on POST",
+			args: validationArgs{
+				r: unknownContentType,
+			},
+			wantErrReason:      "failed to decode request body",
+			wantErrParseKind:   KindUnsupportedFormat,
+			wantErrParseReason: "unsupported content type \"application/xml\"",
+			wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
+				Title: "unsupported content type \"application/xml\""},
+		},
+		{
+			name: "error - unsupported content-type on POST",
+			args: validationArgs{
+				r: unsupportedContentType,
+			},
+			wantErrReason: "header 'Content-Type' has unexpected value: \"text/plain\"",
+			wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
+				Title: "unsupported content type \"text/plain\""},
+		},
+		{
+			name: "success - no content-type header required on GET",
+			args: validationArgs{
+				r: noContentTypeNeeded,
+			},
+		},
+
+		//
+		// Query strings
+		//
+
+		{
+			name: "error - missing required query string parameter",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus", nil),
+			},
+			wantErrParam:   "status",
+			wantErrParamIn: "query",
+			wantErrReason:  "must have a value",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "Parameter 'status' in query is required"},
+		},
+		{
+			name: "error - wrong query string parameter type",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByIds?ids=1,notAnInt", nil),
+			},
+			wantErrParam:   "ids",
+			wantErrParamIn: "query",
+			// This is a nested ParseError. The outer error is a KindOther with no details.
+			// 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: " +
+				"strconv.ParseFloat: parsing \"notAnInt\": 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"},
+		},
+		{
+			name: "success - ignores unknown query string parameter",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?wat=isdis", nil),
+			},
+		},
+		{
+			name: "success - normal case, query strings",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available", nil),
+			},
+		},
+		{
+			name: "success - normal case, query strings, array",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available&status=sold", nil),
+			},
+		},
+		{
+			name: "error - invalid query string array serialization",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available,sold", nil),
+			},
+			wantErrParam:        "status",
+			wantErrParamIn:      "query",
+			wantErrSchemaReason: "JSON value is not one of the allowed values",
+			wantErrSchemaPath:   "/0",
+			wantErrSchemaValue:  "available,sold",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "JSON value is not one of the allowed values",
+				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"}},
+		},
+		{
+			name: "error - invalid enum value for query string parameter",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold&status=watdis", nil),
+			},
+			wantErrParam:        "status",
+			wantErrParamIn:      "query",
+			wantErrSchemaReason: "JSON value is not one of the allowed values",
+			wantErrSchemaPath:   "/1",
+			wantErrSchemaValue:  "watdis",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "JSON value is not one of the allowed values",
+				Detail: "Value 'watdis' at /1 must be one of: available, pending, sold",
+				Source:  &ValidationErrorSource{Parameter: "status"}},
+		},
+		{
+			name: "error - invalid enum value, allowing commas (without 'perhaps you intended' recommendation)",
+			args: validationArgs{
+				// fish,with,commas isn't an enum value
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByKind?kind=dog|fish,with,commas", nil),
+			},
+			wantErrParam:        "kind",
+			wantErrParamIn:      "query",
+			wantErrSchemaReason: "JSON value is not one of the allowed values",
+			wantErrSchemaPath:   "/1",
+			wantErrSchemaValue:  "fish,with,commas",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "JSON value is not one of the allowed values",
+				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"}},
+		},
+		{
+			name: "success - valid enum value, allowing commas",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/findByKind?kind=dog|bird,with,commas", nil),
+			},
+		},
+
+		//
+		// Request bodies
+		//
+
+		{
+			name: "error - missing required object attribute",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama"}`)),
+			},
+			wantErrReason:       "doesn't match the schema",
+			wantErrSchemaReason: "Property 'photoUrls' is missing",
+			wantErrSchemaValue:  map[string]string{"name": "Bahama"},
+			wantErrSchemaPath:   "/",
+			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+				Title: "Property 'photoUrls' is missing",
+				Source:  &ValidationErrorSource{Pointer: "/photoUrls"}},
+		},
+		{
+			name: "error - missing required nested object attribute",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet",
+					bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{}}`)),
+			},
+			wantErrReason:       "doesn't match the schema",
+			wantErrSchemaReason: "Property 'name' is missing",
+			wantErrSchemaValue:  map[string]string{},
+			wantErrSchemaPath:   "/category",
+			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+				Title: "Property 'name' is missing",
+				Source:  &ValidationErrorSource{Pointer: "/category/name"}},
+		},
+		{
+			name: "error - missing required deeply nested object attribute",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet",
+					bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{"tags": [{}]}}`)),
+			},
+			wantErrReason:       "doesn't match the schema",
+			wantErrSchemaReason: "Property 'name' is missing",
+			wantErrSchemaValue:  map[string]string{},
+			wantErrSchemaPath:   "/category/tags/0",
+			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+				Title: "Property 'name' is missing",
+				Source:  &ValidationErrorSource{Pointer: "/category/tags/0/name"}},
+		},
+		{
+			// TODO: Add support for validating readonly properties to upstream validator.
+			name: "error - readonly object attribute",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet",
+					bytes.NewBufferString(`{"id":213,"name":"Bahama","photoUrls":[]}}`)),
+			},
+			//wantErr: true,
+		},
+		{
+			name: "error - wrong attribute type",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet",
+					bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)),
+			},
+			wantErrReason:       "doesn't match the schema",
+			wantErrSchemaReason: "Field must be set to array or not be present",
+			wantErrSchemaPath:   "/photoUrls",
+			wantErrSchemaValue:  "string",
+			// 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,
+				Title: "Field must be set to array or not be present",
+				Source:  &ValidationErrorSource{Pointer: "/photoUrls"}},
+		},
+		{
+			name: "success - ignores unknown object attribute",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet",
+					bytes.NewBufferString(`{"wat":"isdis","name":"Bahama","photoUrls":[]}`)),
+			},
+		},
+		{
+			name: "success - normal case, POST",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet",
+					bytes.NewBufferString(`{"name":"Bahama","photoUrls":[]}`)),
+			},
+		},
+
+		//
+		// Path params
+		//
+
+		{
+			name: "error - missing path param",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/", nil),
+			},
+			wantErrParam:   "petId",
+			wantErrParamIn: "path",
+			wantErrReason:  "must have a value",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title: "Parameter 'petId' in path is required"},
+		},
+		{
+			name: "error - wrong path param type",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/NotAnInt", nil),
+			},
+			wantErrParam:       "petId",
+			wantErrParamIn:     "path",
+			wantErrParseKind:   KindInvalidFormat,
+			wantErrParseValue:  "NotAnInt",
+			wantErrParseReason: "an invalid integer",
+			wantErrResponse: &ValidationError{Status: http.StatusNotFound,
+				Title: "Resource not found with 'petId' value: NotAnInt"},
+		},
+		{
+			name: "success - normal case, with path params",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodGet, "/pet/23", nil),
+			},
+		},
+	}
+}
+
+func TestValidationHandler_validateRequest(t *testing.T) {
+	tests := getValidationTests(t)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := require.New(t)
+
+			h, err := buildValidationHandler(tt)
+			req.NoError(err)
+
+			err = h.validateRequest(tt.args.r)
+			req.Equal(tt.wantErr, err != nil)
+
+			if err != nil {
+				if tt.wantErrBody != "" {
+					req.Equal(tt.wantErrBody, err.Error())
+				}
+
+				if e, ok := err.(*RouteError); ok {
+					req.Equal(tt.wantErrReason, e.Reason)
+					return
+				}
+
+				e, ok := err.(*RequestError)
+				req.True(ok, "error = %v, not a RequestError -- %#v", err, err)
+
+				req.Equal(tt.wantErrReason, e.Reason)
+
+				if e.Parameter != nil {
+					req.Equal(tt.wantErrParam, e.Parameter.Name)
+					req.Equal(tt.wantErrParamIn, e.Parameter.In)
+				} else {
+					req.False(tt.wantErrParam != "" || tt.wantErrParamIn != "",
+						"error = %v, no Parameter -- %#v", e, e)
+				}
+
+				if innerErr, ok := e.Err.(*openapi3.SchemaError); ok {
+					req.Equal(tt.wantErrSchemaReason, innerErr.Reason)
+					pointer := toJSONPointer(innerErr.JSONPointer())
+					req.Equal(tt.wantErrSchemaPath, pointer)
+					req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaValue), fmt.Sprintf("%v", innerErr.Value))
+				} else {
+					req.False(tt.wantErrSchemaReason != "" || tt.wantErrSchemaPath != "",
+						"error = %v, not a SchemaError -- %#v", e.Err, e.Err)
+				}
+
+				if innerErr, ok := e.Err.(*ParseError); ok {
+					req.Equal(tt.wantErrParseKind, innerErr.Kind)
+					req.Equal(tt.wantErrParseValue, innerErr.Value)
+					req.Equal(tt.wantErrParseReason, innerErr.Reason)
+				} else {
+					req.False(tt.wantErrParseValue != nil || tt.wantErrParseReason != "",
+						"error = %v, not a ParseError -- %#v", e.Err, e.Err)
+				}
+			}
+		})
+	}
+}
+
+func TestValidationErrorEncoder(t *testing.T) {
+	tests := getValidationTests(t)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockEncoder := &mockErrorEncoder{}
+			encoder := &ValidationErrorEncoder{Encoder: mockEncoder.Encode}
+
+			req := require.New(t)
+
+			h, err := buildValidationHandler(tt)
+			req.NoError(err)
+
+			err = h.validateRequest(tt.args.r)
+			req.Equal(tt.wantErr, err != nil)
+
+			if err != nil {
+				encoder.Encode(tt.args.r.Context(), err, httptest.NewRecorder())
+				if tt.wantErrResponse != mockEncoder.Err {
+					req.Equal(tt.wantErrResponse, mockEncoder.Err)
+				}
+			}
+		})
+	}
+}
+
+func buildValidationHandler(tt *validationTest) (*ValidationHandler, error) {
+	if tt.fields.SwaggerFile == "" {
+		tt.fields.SwaggerFile = "fixtures/petstore.json"
+	}
+	h := &ValidationHandler{
+		Handler:      tt.fields.Handler,
+		SwaggerFile:  tt.fields.SwaggerFile,
+		ErrorEncoder: tt.fields.ErrorEncoder,
+	}
+	tt.wantErr = tt.wantErr ||
+		(tt.wantErrBody != "") ||
+		(tt.wantErrReason != "") ||
+		(tt.wantErrSchemaReason != "") ||
+		(tt.wantErrSchemaPath != "") ||
+		(tt.wantErrParseValue != nil) ||
+		(tt.wantErrParseReason != "")
+	err := h.Load()
+	return h, err
+}
+
+type testHandler struct {
+	Called bool
+}
+
+func (h *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	h.Called = true
+}
+
+type mockErrorEncoder struct {
+	Called bool
+	Ctx    context.Context
+	Err    error
+	W      http.ResponseWriter
+}
+
+func (e *mockErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) {
+	e.Called = true
+	e.Ctx = ctx
+	e.Err = err
+	e.W = w
+}
+
+func runTest(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response {
+	h := &ValidationHandler{
+		Handler:      handler,
+		ErrorEncoder: encoder,
+		SwaggerFile:  "fixtures/petstore.json",
+	}
+	err := h.Load()
+	require.NoError(t, err)
+	w := httptest.NewRecorder()
+	h.ServeHTTP(w, req)
+	return w.Result()
+}
+
+func TestValidationHandler_ServeHTTP(t *testing.T) {
+	t.Run("errors on invalid requests", func(t *testing.T) {
+		httpCtx := context.WithValue(context.Background(), "pig", "tails")
+		r, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil)
+		require.NoError(t, err)
+		r = r.WithContext(httpCtx)
+
+		handler := &testHandler{}
+		encoder := &mockErrorEncoder{}
+		runTest(t, handler, encoder.Encode, r)
+
+		require.False(t, handler.Called)
+		require.True(t, encoder.Called)
+		require.Equal(t, httpCtx, encoder.Ctx)
+		require.NotNil(t, encoder.Err)
+	})
+
+	t.Run("passes valid requests through", func(t *testing.T) {
+		r := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil)
+
+		handler := &testHandler{}
+		encoder := &mockErrorEncoder{}
+		runTest(t, handler, encoder.Encode, r)
+
+		require.True(t, handler.Called)
+		require.False(t, encoder.Called)
+	})
+
+	t.Run("uses error encoder", func(t *testing.T) {
+		r := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`))
+
+		handler := &testHandler{}
+		encoder := &ValidationErrorEncoder{Encoder: (ErrorEncoder)(DefaultErrorEncoder)}
+		resp := runTest(t, handler, encoder.Encode, r)
+
+		body, err := ioutil.ReadAll(resp.Body)
+		require.NoError(t, err)
+		require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
+		require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body))
+	})
+}
diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go
index 7921592..6ac05b3 100644
--- a/openapi3filter/validation_test.go
+++ b/openapi3filter/validation_test.go
@@ -593,9 +593,9 @@ func TestOperationOrSwaggerSecurity(t *testing.T) {
 
 		// Create the request
 		emptyBody := bytes.NewReader(make([]byte, 0))
-		pathUrl, err := url.Parse(path.name)
+		pathURL, err := url.Parse(path.name)
 		require.NoError(t, err)
-		route, _, err := router.FindRoute(http.MethodGet, pathUrl)
+		route, _, err := router.FindRoute(http.MethodGet, pathURL)
 		require.NoError(t, err)
 		req := openapi3filter.RequestValidationInput{
 			Request: httptest.NewRequest(http.MethodGet, path.name, emptyBody),
@@ -605,7 +605,7 @@ func TestOperationOrSwaggerSecurity(t *testing.T) {
 					if schemesValidated != nil {
 						if validated, ok := (*schemesValidated)[input.SecurityScheme]; ok {
 							if validated {
-								t.Fatalf("The path \"%s\" had the schemes %v named \"%s\" validated more than once",
+								t.Fatalf("The path %q had the schemes %v named %q validated more than once",
 									path.name, input.SecurityScheme, input.SecuritySchemeName)
 							}
 							(*schemesValidated)[input.SecurityScheme] = true
@@ -613,7 +613,7 @@ func TestOperationOrSwaggerSecurity(t *testing.T) {
 						}
 					}
 
-					t.Fatalf("The path \"%s\" had the schemes %v named \"%s\"",
+					t.Fatalf("The path %q had the schemes %v named %q",
 						path.name, input.SecurityScheme, input.SecuritySchemeName)
 
 					return nil
@@ -717,9 +717,9 @@ func TestAnySecurityRequirementMet(t *testing.T) {
 
 	for _, tc := range tc {
 		// Create the request input for the path
-		tcUrl, err := url.Parse(tc.name)
+		tcURL, err := url.Parse(tc.name)
 		require.NoError(t, err)
-		route, _, err := router.FindRoute(http.MethodGet, tcUrl)
+		route, _, err := router.FindRoute(http.MethodGet, tcURL)
 		require.NoError(t, err)
 		req := openapi3filter.RequestValidationInput{
 			Route: route,
@@ -733,9 +733,9 @@ func TestAnySecurityRequirementMet(t *testing.T) {
 
 		// If there should have been an error
 		if tc.error {
-			require.Errorf(t, err, "an error is expected for path \"%s\"", tc.name)
+			require.Errorf(t, err, "an error is expected for path %q", tc.name)
 		} else {
-			require.NoErrorf(t, err, "an error wasn't expected for path \"%s\"", tc.name)
+			require.NoErrorf(t, err, "an error wasn't expected for path %q", tc.name)
 		}
 	}
 }
@@ -817,9 +817,9 @@ func TestAllSchemesMet(t *testing.T) {
 
 	for _, tc := range tc {
 		// Create the request input for the path
-		tcUrl, err := url.Parse(tc.name)
+		tcURL, err := url.Parse(tc.name)
 		require.NoError(t, err)
-		route, _, err := router.FindRoute(http.MethodGet, tcUrl)
+		route, _, err := router.FindRoute(http.MethodGet, tcURL)
 		require.NoError(t, err)
 		req := openapi3filter.RequestValidationInput{
 			Route: route,
@@ -855,9 +855,8 @@ func makeAuthFunc(schemes map[string]bool) func(c context.Context, input *openap
 		if present {
 			// Return an unmet scheme error
 			return fmt.Errorf("security scheme for %q wasn't met", input.SecuritySchemeName)
-		} else {
-			// Return an unknown scheme error
-			return fmt.Errorf("security scheme for %q is unknown", input.SecuritySchemeName)
 		}
+		// Return an unknown scheme error
+		return fmt.Errorf("security scheme for %q is unknown", input.SecuritySchemeName)
 	}
 }