Repository: getkin/kin-openapi
Pull request #258 (2020-10-12 15:44:27)

Adds ipv4 and ipv6 formats support
trash it if too poor quality

RiccardoManfrin: I can rollback to `DefineStringFormat` np. It's gross patch for my stuff to work, didn't care about backward compat, but I agree.
I'd keep the `DefineStrinngCallbackFormat` one instead.. more explicit.. just taste btw. 

Patch:
diff --git a/README.md b/README.md
index 6068391..bc4a733 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Here's some projects that depend on _kin-openapi_:
   * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPI 3 spec
   * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs"
   * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in"
+  * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach."
   * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request)
 
 ## Alternative projects
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index d06d838..fbf2b09 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -2,6 +2,7 @@
 package openapi2conv
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"net/url"
@@ -40,35 +41,40 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 		}
 	}
 
-	if paths := swagger.Paths; paths != nil {
-		resultPaths := make(map[string]*openapi3.PathItem, len(paths))
-		for path, pathItem := range paths {
-			r, err := ToV3PathItem(swagger, pathItem)
-			if err != nil {
-				return nil, err
-			}
-			resultPaths[path] = r
-		}
-		result.Paths = resultPaths
-	}
-
-	if parameters := swagger.Parameters; parameters != nil {
-		result.Components.Parameters = make(map[string]*openapi3.ParameterRef, len(parameters))
-		result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef, len(parameters))
+	result.Components.Schemas = make(map[string]*openapi3.SchemaRef)
+	if parameters := swagger.Parameters; len(parameters) != 0 {
+		result.Components.Parameters = make(map[string]*openapi3.ParameterRef)
+		result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef)
 		for k, parameter := range parameters {
-			v3Parameter, v3RequestBody, err := ToV3Parameter(parameter)
+			v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&result.Components, parameter)
 			switch {
 			case err != nil:
 				return nil, err
 			case v3RequestBody != nil:
 				result.Components.RequestBodies[k] = v3RequestBody
+			case v3SchemaMap != nil:
+				for _, v3Schema := range v3SchemaMap {
+					result.Components.Schemas[k] = v3Schema
+				}
 			default:
 				result.Components.Parameters[k] = v3Parameter
 			}
 		}
 	}
 
-	if responses := swagger.Responses; responses != nil {
+	if paths := swagger.Paths; len(paths) != 0 {
+		resultPaths := make(map[string]*openapi3.PathItem, len(paths))
+		for path, pathItem := range paths {
+			r, err := ToV3PathItem(swagger, &result.Components, pathItem)
+			if err != nil {
+				return nil, err
+			}
+			resultPaths[path] = r
+		}
+		result.Paths = resultPaths
+	}
+
+	if responses := swagger.Responses; len(responses) != 0 {
 		result.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses))
 		for k, response := range responses {
 			r, err := ToV3Response(response)
@@ -79,9 +85,11 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 		}
 	}
 
-	result.Components.Schemas = ToV3Schemas(swagger.Definitions)
+	for key, schema := range ToV3Schemas(swagger.Definitions) {
+		result.Components.Schemas[key] = schema
+	}
 
-	if m := swagger.SecurityDefinitions; m != nil {
+	if m := swagger.SecurityDefinitions; len(m) != 0 {
 		resultSecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef)
 		for k, v := range m {
 			r, err := ToV3SecurityScheme(v)
@@ -94,7 +102,6 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 	}
 
 	result.Security = ToV3SecurityRequirements(swagger.Security)
-
 	{
 		sl := openapi3.NewSwaggerLoader()
 		if err := sl.ResolveRefsIn(result, nil); err != nil {
@@ -104,25 +111,27 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 	return result, nil
 }
 
-func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) {
+func ToV3PathItem(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) {
 	stripNonCustomExtensions(pathItem.Extensions)
 	result := &openapi3.PathItem{
 		ExtensionProps: pathItem.ExtensionProps,
 	}
 	for method, operation := range pathItem.Operations() {
-		resultOperation, err := ToV3Operation(swagger, pathItem, operation)
+		resultOperation, err := ToV3Operation(swagger, components, pathItem, operation)
 		if err != nil {
 			return nil, err
 		}
 		result.SetOperation(method, resultOperation)
 	}
 	for _, parameter := range pathItem.Parameters {
-		v3Parameter, v3RequestBody, err := ToV3Parameter(parameter)
+		v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter)
 		switch {
 		case err != nil:
 			return nil, err
 		case v3RequestBody != nil:
 			return nil, errors.New("pathItem must not have a body parameter")
+		case v3Schema != nil:
+			return nil, errors.New("pathItem must not have a schema parameter")
 		default:
 			result.Parameters = append(result.Parameters, v3Parameter)
 		}
@@ -130,7 +139,7 @@ func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*open
 	return result, nil
 }
 
-func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) {
+func ToV3Operation(swagger *openapi2.Swagger, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) {
 	if operation == nil {
 		return nil, nil
 	}
@@ -148,19 +157,24 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera
 	}
 
 	var reqBodies []*openapi3.RequestBodyRef
+	formDataSchemas := make(map[string]*openapi3.SchemaRef)
 	for _, parameter := range operation.Parameters {
-		v3Parameter, v3RequestBody, err := ToV3Parameter(parameter)
+		v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter)
 		switch {
 		case err != nil:
 			return nil, err
 		case v3RequestBody != nil:
 			reqBodies = append(reqBodies, v3RequestBody)
+		case v3SchemaMap != nil:
+			for key, v3Schema := range v3SchemaMap {
+				formDataSchemas[key] = v3Schema
+			}
 		default:
 			result.Parameters = append(result.Parameters, v3Parameter)
 		}
 	}
 	var err error
-	if result.RequestBody, err = onlyOneReqBodyParam(reqBodies); err != nil {
+	if result.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components); err != nil {
 		return nil, err
 	}
 
@@ -178,9 +192,31 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera
 	return result, nil
 }
 
-func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, error) {
+func getParameterNameFromOldRef(ref string) string {
+	cleanPath := strings.TrimPrefix(ref, "#/parameters/")
+	pathSections := strings.SplitN(cleanPath, "/", 1)
+
+	return pathSections[0]
+}
+
+func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) {
 	if ref := parameter.Ref; ref != "" {
-		return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil
+		if strings.HasPrefix(ref, "#/parameters/") {
+			name := getParameterNameFromOldRef(ref)
+			if _, ok := components.RequestBodies[name]; ok {
+				v3Ref := strings.Replace(ref, "#/parameters/", "#/components/requestBodies/", 1)
+				return nil, &openapi3.RequestBodyRef{Ref: v3Ref}, nil, nil
+			} else if schema, ok := components.Schemas[name]; ok {
+				schemaRefMap := make(map[string]*openapi3.SchemaRef)
+				if val, ok := schema.Value.Extensions["x-formData-name"]; ok {
+					name = val.(string)
+				}
+				v3Ref := strings.Replace(ref, "#/parameters/", "#/components/schemas/", 1)
+				schemaRefMap[name] = &openapi3.SchemaRef{Ref: v3Ref}
+				return nil, nil, schemaRefMap, nil
+			}
+		}
+		return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil
 	}
 	stripNonCustomExtensions(parameter.Extensions)
 
@@ -191,52 +227,63 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 			Required:       parameter.Required,
 			ExtensionProps: parameter.ExtensionProps,
 		}
+		if parameter.Name != "" {
+			result.Extensions["x-originalParamName"] = parameter.Name
+		}
+
 		if schemaRef := parameter.Schema; schemaRef != nil {
 			// Assuming JSON
 			result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
 		}
-		return nil, &openapi3.RequestBodyRef{Value: result}, nil
+		return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil
 
 	case "formData":
 		format, typ := parameter.Format, parameter.Type
 		if typ == "file" {
 			format, typ = "binary", "string"
 		}
-		reqBodyRef := formDataBody(
-			map[string]*openapi3.SchemaRef{
-				parameter.Name: {
-					Value: &openapi3.Schema{
-						Description:     parameter.Description,
-						Type:            typ,
-						ExtensionProps:  parameter.ExtensionProps,
-						Format:          format,
-						Enum:            parameter.Enum,
-						Min:             parameter.Minimum,
-						Max:             parameter.Maximum,
-						ExclusiveMin:    parameter.ExclusiveMin,
-						ExclusiveMax:    parameter.ExclusiveMax,
-						MinLength:       parameter.MinLength,
-						MaxLength:       parameter.MaxLength,
-						Default:         parameter.Default,
-						Items:           parameter.Items,
-						MinItems:        parameter.MinItems,
-						MaxItems:        parameter.MaxItems,
-						Pattern:         parameter.Pattern,
-						AllowEmptyValue: parameter.AllowEmptyValue,
-						UniqueItems:     parameter.UniqueItems,
-						MultipleOf:      parameter.MultipleOf,
-					},
-				},
+		if parameter.ExtensionProps.Extensions == nil {
+			parameter.ExtensionProps.Extensions = make(map[string]interface{})
+		}
+		parameter.ExtensionProps.Extensions["x-formData-name"] = parameter.Name
+		var required []string
+		if parameter.Required {
+			required = []string{parameter.Name}
+		}
+		schemaRef := &openapi3.SchemaRef{
+			Value: &openapi3.Schema{
+				Description:     parameter.Description,
+				Type:            typ,
+				ExtensionProps:  parameter.ExtensionProps,
+				Format:          format,
+				Enum:            parameter.Enum,
+				Min:             parameter.Minimum,
+				Max:             parameter.Maximum,
+				ExclusiveMin:    parameter.ExclusiveMin,
+				ExclusiveMax:    parameter.ExclusiveMax,
+				MinLength:       parameter.MinLength,
+				MaxLength:       parameter.MaxLength,
+				Default:         parameter.Default,
+				Items:           parameter.Items,
+				MinItems:        parameter.MinItems,
+				MaxItems:        parameter.MaxItems,
+				Pattern:         parameter.Pattern,
+				AllowEmptyValue: parameter.AllowEmptyValue,
+				UniqueItems:     parameter.UniqueItems,
+				MultipleOf:      parameter.MultipleOf,
+				Required:        required,
 			},
-			map[string]bool{parameter.Name: parameter.Required},
-		)
-		return nil, reqBodyRef, nil
+		}
+		schemaRefMap := make(map[string]*openapi3.SchemaRef)
+		schemaRefMap[parameter.Name] = schemaRef
+		return nil, nil, schemaRefMap, nil
 
 	default:
 		required := parameter.Required
 		if parameter.In == openapi3.ParameterInPath {
 			required = true
 		}
+
 		result := &openapi3.Parameter{
 			In:             parameter.In,
 			Name:           parameter.Name,
@@ -263,7 +310,7 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 				MultipleOf:      parameter.MultipleOf,
 			}}),
 		}
-		return &openapi3.ParameterRef{Value: result}, nil, nil
+		return &openapi3.ParameterRef{Value: result}, nil, nil, nil
 	}
 }
 
@@ -290,43 +337,59 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool) *
 	}
 }
 
-func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef) (*openapi3.RequestBodyRef, error) {
-	var (
-		body           *openapi3.RequestBodyRef
-		formDataParams map[string]*openapi3.SchemaRef
-		formDataReqs   map[string]bool
-	)
-	for i, requestBodyRef := range bodies {
-		mediaType := requestBodyRef.Value.GetMediaType("multipart/form-data")
-		if mediaType != nil {
-			for name, schemaRef := range mediaType.Schema.Value.Properties {
-				if formDataParams == nil {
-					formDataParams = make(map[string]*openapi3.SchemaRef, len(bodies)-i)
-				}
-				if formDataReqs == nil {
-					formDataReqs = make(map[string]bool, len(bodies)-i)
+func getParameterNameFromNewRef(ref string) string {
+	cleanPath := strings.TrimPrefix(ref, "#/components/schemas/")
+	pathSections := strings.SplitN(cleanPath, "/", 1)
+
+	return pathSections[0]
+}
+
+func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (*openapi3.RequestBodyRef, error) {
+	if len(bodies) > 1 {
+		return nil, errors.New("multiple body parameters cannot exist for the same operation")
+	}
+
+	if len(bodies) != 0 && len(formDataSchemas) != 0 {
+		return nil, errors.New("body and form parameters cannot exist together for the same operation")
+	}
+
+	for _, requestBodyRef := range bodies {
+		return requestBodyRef, nil
+	}
+
+	if len(formDataSchemas) > 0 {
+		formDataParams := make(map[string]*openapi3.SchemaRef, len(formDataSchemas))
+		formDataReqs := make(map[string]bool, len(formDataSchemas))
+		for formDataName, formDataSchema := range formDataSchemas {
+			if formDataSchema.Ref != "" {
+				name := getParameterNameFromNewRef(formDataSchema.Ref)
+				if schema := components.Schemas[name]; schema != nil && schema.Value != nil {
+					if tempName, ok := schema.Value.Extensions["x-formData-name"]; ok {
+						name = tempName.(string)
+					}
+					formDataParams[name] = formDataSchema
+					formDataReqs[name] = false
+					for _, req := range schema.Value.Required {
+						if name == req {
+							formDataReqs[name] = true
+						}
+					}
 				}
-				formDataParams[name] = schemaRef
-				formDataReqs[name] = false
-				for _, req := range mediaType.Schema.Value.Required {
-					if name == req {
-						formDataReqs[name] = true
+			} else if formDataSchema.Value != nil {
+				formDataParams[formDataName] = formDataSchema
+				formDataReqs[formDataName] = false
+				for _, req := range formDataSchema.Value.Required {
+					if formDataName == req {
+						formDataReqs[formDataName] = true
 					}
 				}
-				break
 			}
-		} else {
-			body = requestBodyRef
 		}
-	}
-	switch {
-	case len(formDataParams) != 0 && body != nil:
-		return nil, errors.New("body and form parameters cannot exist together for the same operation")
-	case len(formDataParams) != 0:
+
 		return formDataBody(formDataParams, formDataReqs), nil
-	default:
-		return body, nil
 	}
+
+	return nil, nil
 }
 
 func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
@@ -395,6 +458,8 @@ func FromV3Ref(ref string) string {
 	for new, old := range ref2To3 {
 		if strings.HasPrefix(ref, old) {
 			ref = strings.Replace(ref, old, new, 1)
+		} else if strings.HasPrefix(ref, "#/components/requestBodies/") {
+			ref = strings.Replace(ref, "#/components/requestBodies/", "#/parameters/", 1)
 		}
 	}
 	return ref
@@ -460,16 +525,17 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
 
 // FromV3Swagger converts an OpenAPIv3 spec to an OpenAPIv2 spec
 func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
-	resultResponses, err := FromV3Responses(swagger.Components.Responses)
+	resultResponses, err := FromV3Responses(swagger.Components.Responses, &swagger.Components)
 	if err != nil {
 		return nil, err
 	}
 	stripNonCustomExtensions(swagger.Extensions)
-
+	schemas, parameters := FromV3Schemas(swagger.Components.Schemas, &swagger.Components)
 	result := &openapi2.Swagger{
 		Swagger:        "2.0",
 		Info:           *swagger.Info,
-		Definitions:    FromV3Schemas(swagger.Components.Schemas),
+		Definitions:    schemas,
+		Parameters:     parameters,
 		Responses:      resultResponses,
 		Tags:           swagger.Tags,
 		ExtensionProps: swagger.ExtensionProps,
@@ -520,7 +586,7 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 		}
 		params := openapi2.Parameters{}
 		for _, param := range pathItem.Parameters {
-			p, err := FromV3Parameter(param)
+			p, err := FromV3Parameter(param, &swagger.Components)
 			if err != nil {
 				return nil, err
 			}
@@ -528,12 +594,35 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 		}
 		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 {
+		if result.Parameters[name], err = FromV3Parameter(param, &swagger.Components); err != nil {
 			return nil, err
 		}
 	}
+
+	for name, requestBody := range swagger.Components.RequestBodies {
+		parameters := FromV3RequestBodyFormData(requestBody)
+		for _, param := range parameters {
+			result.Parameters[param.Name] = param
+		}
+
+		if len(parameters) == 0 {
+			paramName := name
+			if requestBody.Value != nil {
+				if originalName, ok := requestBody.Value.Extensions["x-originalParamName"]; ok {
+					json.Unmarshal(originalName.(json.RawMessage), &paramName)
+				}
+			}
+
+			r, err := FromV3RequestBody(swagger, paramName, requestBody)
+			if err != nil {
+				return nil, err
+			}
+			result.Parameters[name] = r
+		}
+	}
+
 	if m := swagger.Components.SecuritySchemes; m != nil {
 		resultSecuritySchemes := make(map[string]*openapi2.SecurityScheme)
 		for id, securityScheme := range m {
@@ -549,34 +638,89 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 	return result, nil
 }
 
-func FromV3Schemas(schemas map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef {
-	v2Defs := make(map[string]*openapi3.SchemaRef, len(schemas))
+func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) {
+	v2Defs := make(map[string]*openapi3.SchemaRef)
+	v2Params := make(map[string]*openapi2.Parameter)
 	for name, schema := range schemas {
-		v2Defs[name] = FromV3SchemaRef(schema)
+		schemaConv, parameterConv := FromV3SchemaRef(schema, components)
+		if schemaConv != nil {
+			v2Defs[name] = schemaConv
+		} else if parameterConv != nil {
+			if parameterConv.Name == "" {
+				parameterConv.Name = name
+			}
+			v2Params[name] = parameterConv
+		}
 	}
-	return v2Defs
+	return v2Defs, v2Params
 }
 
-func FromV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
+func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) {
 	if ref := schema.Ref; ref != "" {
-		return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}
+		name := getParameterNameFromNewRef(ref)
+		if val, ok := components.Schemas[name]; ok {
+			if val.Value.Format == "binary" {
+				v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1)
+				return nil, &openapi2.Parameter{Ref: v2Ref}
+			}
+		}
+
+		return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil
 	}
 	if schema.Value == nil {
-		return schema
+		return schema, nil
+	}
+
+	if schema.Value != nil {
+		if schema.Value.Type == "string" && schema.Value.Format == "binary" {
+			paramType := "file"
+			required := false
+
+			value, _ := schema.Value.Extensions["x-formData-name"]
+			var originalName string
+			json.Unmarshal(value.(json.RawMessage), &originalName)
+			for _, prop := range schema.Value.Required {
+				if originalName == prop {
+					required = true
+				}
+			}
+			return nil, &openapi2.Parameter{
+				In:              "formData",
+				Name:            originalName,
+				Description:     schema.Value.Description,
+				Type:            paramType,
+				Enum:            schema.Value.Enum,
+				Minimum:         schema.Value.Min,
+				Maximum:         schema.Value.Max,
+				ExclusiveMin:    schema.Value.ExclusiveMin,
+				ExclusiveMax:    schema.Value.ExclusiveMax,
+				MinLength:       schema.Value.MinLength,
+				MaxLength:       schema.Value.MaxLength,
+				Default:         schema.Value.Default,
+				Items:           schema.Value.Items,
+				MinItems:        schema.Value.MinItems,
+				MaxItems:        schema.Value.MaxItems,
+				AllowEmptyValue: schema.Value.AllowEmptyValue,
+				UniqueItems:     schema.Value.UniqueItems,
+				MultipleOf:      schema.Value.MultipleOf,
+				ExtensionProps:  schema.Value.ExtensionProps,
+				Required:        required,
+			}
+		}
 	}
 	if v := schema.Value.Items; v != nil {
-		schema.Value.Items = FromV3SchemaRef(v)
+		schema.Value.Items, _ = FromV3SchemaRef(v, components)
 	}
 	for k, v := range schema.Value.Properties {
-		schema.Value.Properties[k] = FromV3SchemaRef(v)
+		schema.Value.Properties[k], _ = FromV3SchemaRef(v, components)
 	}
 	if v := schema.Value.AdditionalProperties; v != nil {
-		schema.Value.AdditionalProperties = FromV3SchemaRef(v)
+		schema.Value.AdditionalProperties, _ = FromV3SchemaRef(v, components)
 	}
 	for i, v := range schema.Value.AllOf {
-		schema.Value.AllOf[i] = FromV3SchemaRef(v)
+		schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components)
 	}
-	return schema
+	return schema, nil
 }
 
 func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements {
@@ -603,7 +747,7 @@ func FromV3PathItem(swagger *openapi3.Swagger, pathItem *openapi3.PathItem) (*op
 		result.SetOperation(method, r)
 	}
 	for _, parameter := range pathItem.Parameters {
-		p, err := FromV3Parameter(parameter)
+		p, err := FromV3Parameter(parameter, &swagger.Components)
 		if err != nil {
 			return nil, err
 		}
@@ -633,6 +777,11 @@ func FromV3RequestBodyFormData(requestBodyRef *openapi3.RequestBodyRef) openapi2
 	}
 	parameters := openapi2.Parameters{}
 	for propName, schemaRef := range mediaType.Schema.Value.Properties {
+		if ref := schemaRef.Ref; ref != "" {
+			v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1)
+			parameters = append(parameters, &openapi2.Parameter{Ref: v2Ref})
+			continue
+		}
 		val := schemaRef.Value
 		typ := val.Type
 		if val.Format == "binary" {
@@ -692,7 +841,7 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (
 		result.Security = &resultSecurity
 	}
 	for _, parameter := range operation.Parameters {
-		r, err := FromV3Parameter(parameter)
+		r, err := FromV3Parameter(parameter, &swagger.Components)
 		if err != nil {
 			return nil, err
 		}
@@ -703,20 +852,28 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (
 		if len(parameters) > 0 {
 			result.Parameters = append(result.Parameters, parameters...)
 		} else {
-			r, err := FromV3RequestBody(swagger, operation, v)
+			// Find parameter name that we can use for the body
+			name := findNameForRequestBody(operation)
+			if name == "" {
+				return nil, errors.New("could not find a name for request body")
+			}
+			r, err := FromV3RequestBody(swagger, name, v)
 			if err != nil {
 				return nil, err
 			}
 			result.Parameters = append(result.Parameters, r)
 		}
 	}
+
 	for _, param := range result.Parameters {
 		if param.Type == "file" {
 			result.Consumes = append(result.Consumes, "multipart/form-data")
+			break
 		}
 	}
+
 	if responses := operation.Responses; responses != nil {
-		resultResponses, err := FromV3Responses(responses)
+		resultResponses, err := FromV3Responses(responses, &swagger.Components)
 		if err != nil {
 			return nil, err
 		}
@@ -725,19 +882,12 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (
 	return result, nil
 }
 
-func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) {
+func FromV3RequestBody(swagger *openapi3.Swagger, name string, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) {
 	if ref := requestBodyRef.Ref; ref != "" {
 		return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil
 	}
 	requestBody := requestBodyRef.Value
 
-	// Find parameter name that we can use for the body
-	name := findNameForRequestBody(operation)
-
-	// If found an available name
-	if name == "" {
-		return nil, errors.New("Could not find a name for request body")
-	}
 	stripNonCustomExtensions(requestBody.Extensions)
 	result := &openapi2.Parameter{
 		In:             "body",
@@ -750,12 +900,12 @@ func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation,
 	// Assuming JSON
 	mediaType := requestBody.GetMediaType("application/json")
 	if mediaType != nil {
-		result.Schema = FromV3SchemaRef(mediaType.Schema)
+		result.Schema, _ = FromV3SchemaRef(mediaType.Schema, &swagger.Components)
 	}
 	return result, nil
 }
 
-func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
+func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error) {
 	if ref := ref.Ref; ref != "" {
 		return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil
 	}
@@ -772,7 +922,7 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
 		ExtensionProps: parameter.ExtensionProps,
 	}
 	if schemaRef := parameter.Schema; schemaRef != nil {
-		schemaRef = FromV3SchemaRef(schemaRef)
+		schemaRef, _ = FromV3SchemaRef(schemaRef, components)
 		schema := schemaRef.Value
 		result.Type = schema.Type
 		result.Format = schema.Format
@@ -796,10 +946,10 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
 	return result, nil
 }
 
-func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*openapi2.Response, error) {
+func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) {
 	v2Responses := make(map[string]*openapi2.Response, len(responses))
 	for k, response := range responses {
-		r, err := FromV3Response(response)
+		r, err := FromV3Response(response, components)
 		if err != nil {
 			return nil, err
 		}
@@ -808,7 +958,7 @@ func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*op
 	return v2Responses, nil
 }
 
-func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
+func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) {
 	if ref := ref.Ref; ref != "" {
 		return &openapi2.Response{Ref: FromV3Ref(ref)}, nil
 	}
@@ -828,7 +978,7 @@ func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
 	}
 	if content := response.Content; content != nil {
 		if ct := content["application/json"]; ct != nil {
-			result.Schema = FromV3SchemaRef(ct.Schema)
+			result.Schema, _ = FromV3SchemaRef(ct.Schema, components)
 		}
 	}
 	return result, nil
diff --git a/openapi3/errors.go b/openapi3/errors.go
new file mode 100644
index 0000000..ce52cd4
--- /dev/null
+++ b/openapi3/errors.go
@@ -0,0 +1,43 @@
+package openapi3
+
+import (
+	"bytes"
+	"errors"
+)
+
+// MultiError is a collection of errors, intended for when
+// multiple issues need to be reported upstream
+type MultiError []error
+
+func (me MultiError) Error() string {
+	buff := &bytes.Buffer{}
+	for _, e := range me {
+		buff.WriteString(e.Error())
+		buff.WriteString(" | ")
+	}
+	return buff.String()
+}
+
+//Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()`
+//It will also return true if any of the contained errors match target
+func (me MultiError) Is(target error) bool {
+	if _, ok := target.(MultiError); ok {
+		return true
+	}
+	for _, e := range me {
+		if errors.Is(e, target) {
+			return true
+		}
+	}
+	return false
+}
+
+//As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type
+func (me MultiError) As(target interface{}) bool {
+	for _, e := range me {
+		if errors.As(e, target) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 8a34282..dd59d3f 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -397,7 +397,7 @@ func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema {
 func (schema *Schema) IsEmpty() bool {
 	if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 ||
 		schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax ||
-		schema.Nullable ||
+		schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue ||
 		schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil ||
 		schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" ||
 		schema.MinItems != 0 || schema.MaxItems != nil ||
@@ -452,6 +452,10 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
 	}
 	stack = append(stack, schema)
 
+	if schema.ReadOnly && schema.WriteOnly {
+		return errors.New("A property MUST NOT be marked as both readOnly and writeOnly being true")
+	}
+
 	for _, item := range schema.OneOf {
 		v := item.Value
 		if v == nil {
@@ -577,37 +581,44 @@ func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
 }
 
 func (schema *Schema) IsMatching(value interface{}) bool {
-	return schema.visitJSON(value, true) == nil
+	settings := newSchemaValidationSettings(FailFast())
+	return schema.visitJSON(settings, value) == nil
 }
 
 func (schema *Schema) IsMatchingJSONBoolean(value bool) bool {
-	return schema.visitJSON(value, true) == nil
+	settings := newSchemaValidationSettings(FailFast())
+	return schema.visitJSON(settings, value) == nil
 }
 
 func (schema *Schema) IsMatchingJSONNumber(value float64) bool {
-	return schema.visitJSON(value, true) == nil
+	settings := newSchemaValidationSettings(FailFast())
+	return schema.visitJSON(settings, value) == nil
 }
 
 func (schema *Schema) IsMatchingJSONString(value string) bool {
-	return schema.visitJSON(value, true) == nil
+	settings := newSchemaValidationSettings(FailFast())
+	return schema.visitJSON(settings, value) == nil
 }
 
 func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool {
-	return schema.visitJSON(value, true) == nil
+	settings := newSchemaValidationSettings(FailFast())
+	return schema.visitJSON(settings, value) == nil
 }
 
 func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool {
-	return schema.visitJSON(value, true) == nil
+	settings := newSchemaValidationSettings(FailFast())
+	return schema.visitJSON(settings, value) == nil
 }
 
-func (schema *Schema) VisitJSON(value interface{}) error {
-	return schema.visitJSON(value, false)
+func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error {
+	settings := newSchemaValidationSettings(opts...)
+	return schema.visitJSON(settings, value)
 }
 
-func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) {
+func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) {
 	switch value := value.(type) {
 	case nil:
-		return schema.visitJSONNull(fast)
+		return schema.visitJSONNull(settings)
 	case float64:
 		if math.IsNaN(value) {
 			return ErrSchemaInputNaN
@@ -620,23 +631,23 @@ func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) {
 	if schema.IsEmpty() {
 		return
 	}
-	if err = schema.visitSetOperations(value, fast); err != nil {
+	if err = schema.visitSetOperations(settings, value); err != nil {
 		return
 	}
 
 	switch value := value.(type) {
 	case nil:
-		return schema.visitJSONNull(fast)
+		return schema.visitJSONNull(settings)
 	case bool:
-		return schema.visitJSONBoolean(value, fast)
+		return schema.visitJSONBoolean(settings, value)
 	case float64:
-		return schema.visitJSONNumber(value, fast)
+		return schema.visitJSONNumber(settings, value)
 	case string:
-		return schema.visitJSONString(value, fast)
+		return schema.visitJSONString(settings, value)
 	case []interface{}:
-		return schema.visitJSONArray(value, fast)
+		return schema.visitJSONArray(settings, value)
 	case map[string]interface{}:
-		return schema.visitJSONObject(value, fast)
+		return schema.visitJSONObject(settings, value)
 	default:
 		return &SchemaError{
 			Value:       value,
@@ -647,14 +658,16 @@ func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) {
 	}
 }
 
-func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err error) {
+func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) {
+	var oldfailfast bool
+
 	if enum := schema.Enum; len(enum) != 0 {
 		for _, v := range enum {
 			if value == v {
 				return
 			}
 		}
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
 		return &SchemaError{
@@ -670,8 +683,9 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro
 		if v == nil {
 			return foundUnresolvedRef(ref.Ref)
 		}
-		if err := v.visitJSON(value, true); err == nil {
-			if fast {
+		oldfailfast, settings.failfast = settings.failfast, true
+		if err := v.visitJSON(settings, value); err == nil {
+			if oldfailfast {
 				return errSchema
 			}
 			return &SchemaError{
@@ -680,6 +694,7 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro
 				SchemaField: "not",
 			}
 		}
+		settings.failfast = oldfailfast
 	}
 
 	if v := schema.OneOf; len(v) > 0 {
@@ -689,12 +704,14 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro
 			if v == nil {
 				return foundUnresolvedRef(item.Ref)
 			}
-			if err := v.visitJSON(value, true); err == nil {
+			oldfailfast, settings.failfast = settings.failfast, true
+			if err := v.visitJSON(settings, value); err == nil {
 				ok++
 			}
+			settings.failfast = oldfailfast
 		}
 		if ok != 1 {
-			if fast {
+			if settings.failfast {
 				return errSchema
 			}
 			return &SchemaError{
@@ -712,13 +729,15 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro
 			if v == nil {
 				return foundUnresolvedRef(item.Ref)
 			}
-			if err := v.visitJSON(value, true); err == nil {
+			oldfailfast, settings.failfast = settings.failfast, true
+			if err := v.visitJSON(settings, value); err == nil {
 				ok = true
 				break
 			}
+			settings.failfast = oldfailfast
 		}
 		if !ok {
-			if fast {
+			if settings.failfast {
 				return errSchema
 			}
 			return &SchemaError{
@@ -734,8 +753,9 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro
 		if v == nil {
 			return foundUnresolvedRef(item.Ref)
 		}
-		if err := v.visitJSON(value, false); err != nil {
-			if fast {
+		oldfailfast, settings.failfast = settings.failfast, false
+		if err := v.visitJSON(settings, value); err != nil {
+			if oldfailfast {
 				return errSchema
 			}
 			return &SchemaError{
@@ -745,15 +765,16 @@ func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err erro
 				Origin:      err,
 			}
 		}
+		settings.failfast = oldfailfast
 	}
 	return
 }
 
-func (schema *Schema) visitJSONNull(fast bool) (err error) {
+func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) {
 	if schema.Nullable {
 		return
 	}
-	if fast {
+	if settings.failfast {
 		return errSchema
 	}
 	return &SchemaError{
@@ -765,88 +786,111 @@ func (schema *Schema) visitJSONNull(fast bool) (err error) {
 }
 
 func (schema *Schema) VisitJSONBoolean(value bool) error {
-	return schema.visitJSONBoolean(value, false)
+	settings := newSchemaValidationSettings()
+	return schema.visitJSONBoolean(settings, value)
 }
 
-func (schema *Schema) visitJSONBoolean(value bool, fast bool) (err error) {
+func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) {
 	if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" {
-		return schema.expectedType("boolean", fast)
+		return schema.expectedType(settings, "boolean")
 	}
 	return
 }
 
 func (schema *Schema) VisitJSONNumber(value float64) error {
-	return schema.visitJSONNumber(value, false)
+	settings := newSchemaValidationSettings()
+	return schema.visitJSONNumber(settings, value)
 }
 
-func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) {
+func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error {
+	var me MultiError
 	schemaType := schema.Type
 	if schemaType == "integer" {
 		if bigFloat := big.NewFloat(value); !bigFloat.IsInt() {
-			if fast {
+			if settings.failfast {
 				return errSchema
 			}
-			return &SchemaError{
+			err := &SchemaError{
 				Value:       value,
 				Schema:      schema,
 				SchemaField: "type",
 				Reason:      "Value must be an integer",
 			}
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
 		}
 	} else if schemaType != "" && schemaType != "number" {
-		return schema.expectedType("number, integer", fast)
+		return schema.expectedType(settings, "number, integer")
 	}
 
 	// "exclusiveMinimum"
 	if v := schema.ExclusiveMin; v && !(*schema.Min < value) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "exclusiveMinimum",
 			Reason:      fmt.Sprintf("Number must be more than %g", *schema.Min),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "exclusiveMaximum"
 	if v := schema.ExclusiveMax; v && !(*schema.Max > value) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "exclusiveMaximum",
 			Reason:      fmt.Sprintf("Number must be less than %g", *schema.Max),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "minimum"
 	if v := schema.Min; v != nil && !(*v <= value) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "minimum",
 			Reason:      fmt.Sprintf("Number must be at least %g", *v),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "maximum"
 	if v := schema.Max; v != nil && !(*v >= value) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "maximum",
 			Reason:      fmt.Sprintf("Number must be most %g", *v),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "multipleOf"
@@ -854,28 +898,40 @@ func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) {
 		// "A numeric instance is valid only if division by this keyword's
 		//    value results in an integer."
 		if bigFloat := big.NewFloat(value / *v); !bigFloat.IsInt() {
-			if fast {
+			if settings.failfast {
 				return errSchema
 			}
-			return &SchemaError{
+			err := &SchemaError{
 				Value:       value,
 				Schema:      schema,
 				SchemaField: "multipleOf",
 			}
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
 		}
 	}
-	return
+
+	if len(me) > 0 {
+		return me
+	}
+
+	return nil
 }
 
 func (schema *Schema) VisitJSONString(value string) error {
-	return schema.visitJSONString(value, false)
+	settings := newSchemaValidationSettings()
+	return schema.visitJSONString(settings, value)
 }
 
-func (schema *Schema) visitJSONString(value string, fast bool) (err error) {
+func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error {
 	if schemaType := schema.Type; schemaType != "" && schemaType != "string" {
-		return schema.expectedType("string", fast)
+		return schema.expectedType(settings, "string")
 	}
 
+	var me MultiError
+
 	// "minLength" and "maxLength"
 	minLength := schema.MinLength
 	maxLength := schema.MaxLength
@@ -890,26 +946,34 @@ func (schema *Schema) visitJSONString(value string, fast bool) (err error) {
 			}
 		}
 		if minLength != 0 && length < int64(minLength) {
-			if fast {
+			if settings.failfast {
 				return errSchema
 			}
-			return &SchemaError{
+			err := &SchemaError{
 				Value:       value,
 				Schema:      schema,
 				SchemaField: "minLength",
 				Reason:      fmt.Sprintf("Minimum string length is %d", minLength),
 			}
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
 		}
 		if maxLength != nil && length > int64(*maxLength) {
-			if fast {
+			if settings.failfast {
 				return errSchema
 			}
-			return &SchemaError{
+			err := &SchemaError{
 				Value:       value,
 				Schema:      schema,
 				SchemaField: "maxLength",
 				Reason:      fmt.Sprintf("Maximum string length is %d", *maxLength),
 			}
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
 		}
 	}
 
@@ -930,13 +994,18 @@ func (schema *Schema) visitJSONString(value string, fast bool) (err error) {
 			schema.compiledPattern = cp
 		} else if v := schema.Format; len(v) > 0 {
 			// No pattern, but does have a format
-			re := SchemaStringFormats[v]
-			if re != nil {
-				cp = &compiledPattern{
-					Regexp:    re,
-					ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + re.String() + "`)'",
+			if f, ok := SchemaStringFormats[v]; ok {
+				if f.regexp != nil && f.callback == nil {
+					schema.compiledPattern = &compiledPattern{
+						Regexp:    f.regexp,
+						ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + f.regexp.String() + "`)'",
+					}
+
+				} else if f.regexp == nil && f.callback != nil {
+					return f.callback(value)
+				} else {
+					return fmt.Errorf("corrupted entry %q in SchemaStringFormats", v)
 				}
-				schema.compiledPattern = cp
 			}
 		}
 	}
@@ -946,52 +1015,72 @@ func (schema *Schema) visitJSONString(value string, fast bool) (err error) {
 			if schema.Pattern != "" {
 				field = "pattern"
 			}
-			return &SchemaError{
+			err := &SchemaError{
 				Value:       value,
 				Schema:      schema,
 				SchemaField: field,
 				Reason:      cp.ErrReason,
 			}
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
 		}
 	}
-	return
+
+	if len(me) > 0 {
+		return me
+	}
+
+	return nil
 }
 
 func (schema *Schema) VisitJSONArray(value []interface{}) error {
-	return schema.visitJSONArray(value, false)
+	settings := newSchemaValidationSettings()
+	return schema.visitJSONArray(settings, value)
 }
 
-func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) {
+func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error {
 	if schemaType := schema.Type; schemaType != "" && schemaType != "array" {
-		return schema.expectedType("array", fast)
+		return schema.expectedType(settings, "array")
 	}
 
+	var me MultiError
+
 	lenValue := int64(len(value))
 
 	// "minItems"
 	if v := schema.MinItems; v != 0 && lenValue < int64(v) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "minItems",
 			Reason:      fmt.Sprintf("Minimum number of items is %d", v),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "maxItems"
 	if v := schema.MaxItems; v != nil && lenValue > int64(*v) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "maxItems",
 			Reason:      fmt.Sprintf("Maximum number of items is %d", *v),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "uniqueItems"
@@ -999,15 +1088,19 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error)
 		sliceUniqueItemsChecker = isSliceOfUniqueItems
 	}
 	if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "uniqueItems",
 			Reason:      fmt.Sprintf("Duplicate items found"),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "items"
@@ -1017,51 +1110,75 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error)
 			return foundUnresolvedRef(itemSchemaRef.Ref)
 		}
 		for i, item := range value {
-			if err := itemSchema.VisitJSON(item); err != nil {
-				return markSchemaErrorIndex(err, i)
+			if err := itemSchema.visitJSON(settings, item); err != nil {
+				err = markSchemaErrorIndex(err, i)
+				if !settings.multiError {
+					return err
+				}
+				if itemMe, ok := err.(MultiError); ok {
+					me = append(me, itemMe...)
+				} else {
+					me = append(me, err)
+				}
 			}
 		}
 	}
-	return
+
+	if len(me) > 0 {
+		return me
+	}
+
+	return nil
 }
 
 func (schema *Schema) VisitJSONObject(value map[string]interface{}) error {
-	return schema.visitJSONObject(value, false)
+	settings := newSchemaValidationSettings()
+	return schema.visitJSONObject(settings, value)
 }
 
-func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) (err error) {
+func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error {
 	if schemaType := schema.Type; schemaType != "" && schemaType != "object" {
-		return schema.expectedType("object", fast)
+		return schema.expectedType(settings, "object")
 	}
 
+	var me MultiError
+
 	// "properties"
 	properties := schema.Properties
 	lenValue := int64(len(value))
 
 	// "minProperties"
 	if v := schema.MinProps; v != 0 && lenValue < int64(v) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "minProperties",
 			Reason:      fmt.Sprintf("There must be at least %d properties", v),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "maxProperties"
 	if v := schema.MaxProps; v != nil && lenValue > int64(*v) {
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "maxProperties",
 			Reason:      fmt.Sprintf("There must be at most %d properties", *v),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
 
 	// "additionalProperties"
@@ -1077,11 +1194,19 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) (
 				if p == nil {
 					return foundUnresolvedRef(propertyRef.Ref)
 				}
-				if err := p.VisitJSON(v); err != nil {
-					if fast {
+				if err := p.visitJSON(settings, v); err != nil {
+					if settings.failfast {
 						return errSchema
 					}
-					return markSchemaErrorKey(err, k)
+					err = markSchemaErrorKey(err, k)
+					if !settings.multiError {
+						return err
+					}
+					if v, ok := err.(MultiError); ok {
+						me = append(me, v...)
+						continue
+					}
+					me = append(me, err)
 				}
 				continue
 			}
@@ -1089,43 +1214,72 @@ func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) (
 		allowed := schema.AdditionalPropertiesAllowed
 		if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) {
 			if additionalProperties != nil {
-				if err := additionalProperties.VisitJSON(v); err != nil {
-					if fast {
+				if err := additionalProperties.visitJSON(settings, v); err != nil {
+					if settings.failfast {
 						return errSchema
 					}
-					return markSchemaErrorKey(err, k)
+					err = markSchemaErrorKey(err, k)
+					if !settings.multiError {
+						return err
+					}
+					if v, ok := err.(MultiError); ok {
+						me = append(me, v...)
+						continue
+					}
+					me = append(me, err)
 				}
 			}
 			continue
 		}
-		if fast {
+		if settings.failfast {
 			return errSchema
 		}
-		return &SchemaError{
+		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "properties",
 			Reason:      fmt.Sprintf("Property '%s' is unsupported", k),
 		}
+		if !settings.multiError {
+			return err
+		}
+		me = append(me, err)
 	}
+
+	// "required"
 	for _, k := range schema.Required {
 		if _, ok := value[k]; !ok {
-			if fast {
+			if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq {
+				continue
+			}
+			if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep {
+				continue
+			}
+			if settings.failfast {
 				return errSchema
 			}
-			return markSchemaErrorKey(&SchemaError{
+			err := markSchemaErrorKey(&SchemaError{
 				Value:       value,
 				Schema:      schema,
 				SchemaField: "required",
 				Reason:      fmt.Sprintf("Property '%s' is missing", k),
 			}, k)
+			if !settings.multiError {
+				return err
+			}
+			me = append(me, err)
 		}
 	}
-	return
+
+	if len(me) > 0 {
+		return me
+	}
+
+	return nil
 }
 
-func (schema *Schema) expectedType(typ string, fast bool) error {
-	if fast {
+func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error {
+	if settings.failfast {
 		return errSchema
 	}
 	return &SchemaError{
@@ -1150,6 +1304,12 @@ func markSchemaErrorKey(err error, key string) error {
 		v.reversePath = append(v.reversePath, key)
 		return v
 	}
+	if v, ok := err.(MultiError); ok {
+		for _, e := range v {
+			_ = markSchemaErrorKey(e, key)
+		}
+		return v
+	}
 	return err
 }
 
@@ -1158,6 +1318,12 @@ func markSchemaErrorIndex(err error, index int) error {
 		v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10))
 		return v
 	}
+	if v, ok := err.(MultiError); ok {
+		for _, e := range v {
+			_ = markSchemaErrorIndex(e, index)
+		}
+		return v
+	}
 	return err
 }
 
diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go
index 746e408..ab2992b 100644
--- a/openapi3/schema_formats.go
+++ b/openapi3/schema_formats.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"fmt"
+	"net"
 	"regexp"
 )
 
@@ -10,15 +11,70 @@ const (
 	FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`
 )
 
-var SchemaStringFormats = make(map[string]*regexp.Regexp, 8)
+//FormatCallback custom check on exotic formats
+type FormatCallback func(Val string) error
 
+type Format struct {
+	regexp   *regexp.Regexp
+	callback FormatCallback
+}
+
+//SchemaStringFormats allows for validating strings format
+var SchemaStringFormats = make(map[string]Format, 8)
+
+//DefineStringFormat Defines a new regexp pattern for a given format
 func DefineStringFormat(name string, pattern string) {
 	re, err := regexp.Compile(pattern)
 	if err != nil {
 		err := fmt.Errorf("Format '%v' has invalid pattern '%v': %v", name, pattern, err)
 		panic(err)
 	}
-	SchemaStringFormats[name] = re
+	SchemaStringFormats[name] = Format{regexp: re}
+}
+
+// DefineStringFormatCallback adds a validation function for a specific schema format entry
+func DefineStringFormatCallback(name string, callback FormatCallback) {
+	SchemaStringFormats[name] = Format{callback: callback}
+}
+
+func validateIP(ip string) (*net.IP, error) {
+	parsed := net.ParseIP(ip)
+	if parsed == nil {
+		return nil, &SchemaError{
+			Value:  ip,
+			Reason: "Not an IP address",
+		}
+	}
+	return &parsed, nil
+}
+
+func validateIPv4(ip string) error {
+	parsed, err := validateIP(ip)
+	if err != nil {
+		return err
+	}
+
+	if parsed.To4() == nil {
+		return &SchemaError{
+			Value:  ip,
+			Reason: "Not an IPv4 address (it's IPv6)",
+		}
+	}
+	return nil
+}
+func validateIPv6(ip string) error {
+	parsed, err := validateIP(ip)
+	if err != nil {
+		return err
+	}
+
+	if parsed.To4() != nil {
+		return &SchemaError{
+			Value:  ip,
+			Reason: "Not an IPv6 address (it's IPv4)",
+		}
+	}
+	return nil
 }
 
 func init() {
@@ -35,4 +91,15 @@ func init() {
 
 	// date-time
 	DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`)
+
+}
+
+// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
+func DefineIPv4Format() {
+	DefineStringFormatCallback("ipv4", validateIPv4)
+}
+
+// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
+func DefineIPv6Format() {
+	DefineStringFormatCallback("ipv6", validateIPv6)
 }
diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go
new file mode 100644
index 0000000..71db5f2
--- /dev/null
+++ b/openapi3/schema_validation_settings.go
@@ -0,0 +1,34 @@
+package openapi3
+
+// SchemaValidationOption describes options a user has when validating request / response bodies.
+type SchemaValidationOption func(*schemaValidationSettings)
+
+type schemaValidationSettings struct {
+	failfast     bool
+	multiError   bool
+	asreq, asrep bool // exclusive (XOR) fields
+}
+
+// FailFast returns schema validation errors quicker.
+func FailFast() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.failfast = true }
+}
+
+func MultiErrors() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.multiError = true }
+}
+
+func VisitAsRequest() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false }
+}
+func VisitAsResponse() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true }
+}
+
+func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings {
+	settings := &schemaValidationSettings{}
+	for _, opt := range opts {
+		opt(settings)
+	}
+	return settings
+}
diff --git a/openapi3filter/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json
index 6c229a6..398e9b8 100644
--- a/openapi3filter/fixtures/petstore.json
+++ b/openapi3filter/fixtures/petstore.json
@@ -67,7 +67,21 @@
         ],
         "requestBody": {
           "$ref": "#/components/requestBodies/PetWithRequired"
-        }
+        },
+        "parameters": [
+          {
+            "schema": {
+              "type": "string",
+              "enum": [
+                "demo",
+                "prod"
+              ]
+            },
+            "in": "header",
+            "name": "x-environment",
+            "description": "Where to send the data for processing"
+          }
+        ]
       },
       "patch": {
         "tags": [
@@ -1136,7 +1150,7 @@
           },
           "name": {
             "type": "string",
-            "example": "doggie",
+            "example": "doggie"
           },
           "photoUrls": {
             "type": "array",
@@ -1196,7 +1210,7 @@
           },
           "name": {
             "type": "string",
-            "example": "doggie",
+            "example": "doggie"
           },
           "photoUrls": {
             "type": "array",
@@ -1330,4 +1344,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/openapi3filter/options.go b/openapi3filter/options.go
index 510b777..60e5475 100644
--- a/openapi3filter/options.go
+++ b/openapi3filter/options.go
@@ -10,5 +10,6 @@ type Options struct {
 	ExcludeRequestBody    bool
 	ExcludeResponseBody   bool
 	IncludeResponseStatus bool
+	MultiError            bool
 	AuthenticationFunc    func(c context.Context, input *AuthenticationInput) error
 }
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index f22e71e..0af54c2 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -22,6 +22,11 @@ var ErrInvalidRequired = errors.New("must have a value")
 // Note: One can tune the behavior of uniqueItems: true verification
 // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
 func ValidateRequest(c context.Context, input *RequestValidationInput) error {
+	var (
+		err error
+		me  openapi3.MultiError
+	)
+
 	options := input.Options
 	if options == nil {
 		options = DefaultOptions
@@ -45,24 +50,37 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error {
 				continue
 			}
 		}
-		if err := ValidateParameter(c, input, parameter); err != nil {
+
+		if err = ValidateParameter(c, input, parameter); err != nil && !options.MultiError {
 			return err
 		}
+
+		if err != nil {
+			me = append(me, err)
+		}
 	}
 
 	// For each parameter of the Operation
 	for _, parameter := range operationParameters {
-		if err := ValidateParameter(c, input, parameter.Value); err != nil {
+		if err = ValidateParameter(c, input, parameter.Value); err != nil && !options.MultiError {
 			return err
 		}
+
+		if err != nil {
+			me = append(me, err)
+		}
 	}
 
 	// RequestBody
 	requestBody := operation.RequestBody
 	if requestBody != nil && !options.ExcludeRequestBody {
-		if err := ValidateRequestBody(c, input, requestBody.Value); err != nil {
+		if err = ValidateRequestBody(c, input, requestBody.Value); err != nil && !options.MultiError {
 			return err
 		}
+
+		if err != nil {
+			me = append(me, err)
+		}
 	}
 
 	// Security
@@ -76,10 +94,19 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error {
 		security = &route.Swagger.Security
 	}
 	if security != nil {
-		if err := ValidateSecurityRequirements(c, input, *security); err != nil {
+		if err = ValidateSecurityRequirements(c, input, *security); err != nil && !options.MultiError {
 			return err
 		}
+
+		if err != nil {
+			me = append(me, err)
+		}
 	}
+
+	if len(me) > 0 {
+		return me
+	}
+
 	return nil
 }
 
@@ -95,6 +122,11 @@ func ValidateParameter(c context.Context, input *RequestValidationInput, paramet
 		return nil
 	}
 
+	options := input.Options
+	if options == nil {
+		options = DefaultOptions
+	}
+
 	var value interface{}
 	var err error
 	var schema *openapi3.Schema
@@ -121,7 +153,13 @@ func ValidateParameter(c context.Context, input *RequestValidationInput, paramet
 		// A parameter's schema is not defined so skip validation of a parameter's value.
 		return nil
 	}
-	if err = schema.VisitJSON(value); err != nil {
+
+	var opts []openapi3.SchemaValidationOption
+	if options.MultiError {
+		opts = make([]openapi3.SchemaValidationOption, 0, 1)
+		opts = append(opts, openapi3.MultiErrors())
+	}
+	if err = schema.VisitJSON(value, opts...); err != nil {
 		return &RequestError{Input: input, Parameter: parameter, Err: err}
 	}
 	return nil
@@ -137,6 +175,11 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque
 		data []byte
 	)
 
+	options := input.Options
+	if options == nil {
+		options = DefaultOptions
+	}
+
 	if req.Body != http.NoBody && req.Body != nil {
 		defer req.Body.Close()
 		var err error
@@ -191,8 +234,14 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque
 		}
 	}
 
+	opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here
+	opts = append(opts, openapi3.VisitAsRequest())
+	if options.MultiError {
+		opts = append(opts, openapi3.MultiErrors())
+	}
+
 	// Validate JSON with the schema
-	if err := contentType.Schema.Value.VisitJSON(value); err != nil {
+	if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
 		return &RequestError{
 			Input:       input,
 			RequestBody: requestBody,
diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go
index dad0864..f203802 100644
--- a/openapi3filter/validate_response.go
+++ b/openapi3filter/validate_response.go
@@ -128,8 +128,14 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error {
 		}
 	}
 
+	opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here
+	opts = append(opts, openapi3.VisitAsRequest())
+	if options.MultiError {
+		opts = append(opts, openapi3.MultiErrors())
+	}
+
 	// Validate data with the schema.
-	if err := contentType.Schema.Value.VisitJSON(value); err != nil {
+	if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
 		return &ResponseError{
 			Input:  input,
 			Reason: "response body doesn't match the schema",
diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go
index 6bde134..34e4af9 100644
--- a/openapi3filter/validation_error_encoder.go
+++ b/openapi3filter/validation_error_encoder.go
@@ -148,7 +148,7 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida
 	}
 
 	// Add error source
-	if e.Parameter != nil && e.Parameter.In == "query" {
+	if e.Parameter != nil {
 		// We have a JSONPointer in the query param too so need to
 		// make sure 'Parameter' check takes priority over 'Pointer'
 		cErr.Source = &ValidationErrorSource{
@@ -172,7 +172,8 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida
 		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) &&
+		if e.Parameter != nil &&
+			(e.Parameter.Explode == nil || *e.Parameter.Explode == true) &&
 			(e.Parameter.Style == "" || e.Parameter.Style == "form") &&
 			strings.Contains(value, ",") {
 			parts := strings.Split(value, ",")


Test patch:
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index 8ccc32c..c5379ae 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -94,6 +94,24 @@ const exampleV2 = `
       "name": "banana",
       "required": true,
       "type": "string"
+    },
+    "put_body": {
+      "in": "body",
+      "name": "banana",
+      "required": true,
+      "schema": {
+        "type": "string"
+      },
+      "x-originalParamName":"banana"
+    },
+    "post_form_ref": {
+      "required": true,
+      "description": "param description",
+      "in": "formData",
+      "name": "fileUpload2",
+      "type": "file",
+      "x-formData-name":"fileUpload2",
+      "x-mimetype": "text/plain"
     }
   },
   "paths": {
@@ -214,6 +232,7 @@ const exampleV2 = `
             "schema": {
               "allOf": [{"$ref": "#/definitions/Item"}]
             },
+            "x-originalParamName":"body",
             "x-requestBody": "requestbody extension 1"
           }
         ],
@@ -238,13 +257,18 @@ const exampleV2 = `
             "in": "formData",
             "name": "fileUpload",
             "type": "file",
+            "x-formData-name":"fileUpload", 
             "x-mimetype": "text/plain"
           },
           {
             "description": "Description of file contents",
             "in": "formData",
             "name": "note",
-            "type": "integer"
+            "type": "integer",
+            "x-formData-name":"note" 
+          },
+          {
+            "$ref": "#/parameters/post_form_ref"
           }
         ],
         "responses": {
@@ -255,6 +279,11 @@ const exampleV2 = `
       },
       "put": {
         "description": "example put",
+        "parameters": [
+          { 
+            "$ref": "#/parameters/put_body"
+          }
+        ],
         "responses": {
           "default": {
             "description": "default response"
@@ -309,6 +338,19 @@ const exampleV3 = `
         }
       }
     },
+    "requestBodies": {
+      "put_body": {
+        "content":{ 
+          "application/json": {
+            "schema": {
+              "type": "string"
+            }
+          }
+        },
+        "required": true,
+        "x-originalParamName":"banana"
+      }
+    },
     "responses": {
       "ForbiddenError": {
         "content": {
@@ -349,6 +391,14 @@ const exampleV3 = `
       "ItemExtension": {
         "type": "boolean",
         "description": "It could be anything."
+      },
+      "post_form_ref": {
+        "description": "param description",
+        "format": "binary",
+        "required": ["fileUpload2"],
+        "type": "string",
+        "x-formData-name": "fileUpload2",
+        "x-mimetype":"text/plain"
       }
     }
   },
@@ -491,6 +541,7 @@ const exampleV3 = `
               }
             }
           },
+          "x-originalParamName":"body",
           "x-requestBody": "requestbody extension 1"
         },
         "responses": {
@@ -520,13 +571,19 @@ const exampleV3 = `
                     "description": "param description",
                     "type": "string",
                     "format": "binary",
+                    "x-formData-name":"fileUpload",
                     "x-mimetype": "text/plain"
                   },
                   "note": {
                     "description": "Description of file contents",
-                    "type": "integer"
+                    "type": "integer",
+                    "x-formData-name": "note"
+                  }, 
+                  "fileUpload2": {
+                    "$ref": "#/components/schemas/post_form_ref"
                   }
                 },
+                "required": ["fileUpload2"],
                 "type": "object"
               }
             }
@@ -540,6 +597,9 @@ const exampleV3 = `
       },
       "put": {
         "description": "example put",
+        "requestBody": {
+          "$ref": "#/components/requestBodies/put_body"
+        },
         "responses": {
           "default": {
             "description": "default response"
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index 10e1d05..6650ad5 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -4,7 +4,9 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"math"
+	"reflect"
 	"strings"
 	"testing"
 
@@ -59,13 +61,13 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) {
 	}
 }
 
-func validateSchema(t *testing.T, schema *Schema, value interface{}) error {
+func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error {
 	data, err := json.Marshal(value)
 	require.NoError(t, err)
 	var val interface{}
 	err = json.Unmarshal(data, &val)
 	require.NoError(t, err)
-	return schema.VisitJSON(val)
+	return schema.VisitJSON(val, opts...)
 }
 
 var schemaExamples = []schemaExample{
@@ -1042,3 +1044,151 @@ var schemaErrorExamples = []schemaErrorExample{
 		Want: "NEST",
 	},
 }
+
+type schemaMultiErrorExample struct {
+	Title          string
+	Schema         *Schema
+	Values         []interface{}
+	ExpectedErrors []MultiError
+}
+
+func TestSchemasMultiError(t *testing.T) {
+	for _, example := range schemaMultiErrorExamples {
+		t.Run(example.Title, testSchemaMultiError(t, example))
+	}
+}
+
+func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*testing.T) {
+	return func(t *testing.T) {
+		schema := example.Schema
+		for i, value := range example.Values {
+			err := validateSchema(t, schema, value, MultiErrors())
+			require.Error(t, err)
+			require.IsType(t, MultiError{}, err)
+
+			merr, _ := err.(MultiError)
+			expected := example.ExpectedErrors[i]
+			require.True(t, len(merr) > 0)
+			require.Len(t, merr, len(expected))
+			for _, e := range merr {
+				require.IsType(t, &SchemaError{}, e)
+				var found bool
+				scherr, _ := e.(*SchemaError)
+				for _, expectedErr := range expected {
+					expectedScherr, _ := expectedErr.(*SchemaError)
+					if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) &&
+						expectedScherr.SchemaField == scherr.SchemaField {
+						found = true
+						break
+					}
+				}
+				require.True(t, found, fmt.Sprintf("Missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), ".")))
+			}
+		}
+	}
+}
+
+var schemaMultiErrorExamples = []schemaMultiErrorExample{
+	{
+		Title: "STRING",
+		Schema: NewStringSchema().
+			WithMinLength(2).
+			WithMaxLength(3).
+			WithPattern("^[abc]+$"),
+		Values: []interface{}{
+			"f",
+			"foobar",
+		},
+		ExpectedErrors: []MultiError{
+			{&SchemaError{SchemaField: "minLength"}, &SchemaError{SchemaField: "pattern"}},
+			{&SchemaError{SchemaField: "maxLength"}, &SchemaError{SchemaField: "pattern"}},
+		},
+	},
+	{
+		Title: "NUMBER",
+		Schema: NewIntegerSchema().
+			WithMin(1).
+			WithMax(10),
+		Values: []interface{}{
+			0.5,
+			10.1,
+		},
+		ExpectedErrors: []MultiError{
+			{&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "minimum"}},
+			{&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "maximum"}},
+		},
+	},
+	{
+		Title: "ARRAY: simple",
+		Schema: NewArraySchema().
+			WithMinItems(2).
+			WithMaxItems(2).
+			WithItems(NewStringSchema().
+				WithPattern("^[abc]+$")),
+		Values: []interface{}{
+			[]interface{}{"foo"},
+			[]interface{}{"foo", "bar", "fizz"},
+		},
+		ExpectedErrors: []MultiError{
+			{
+				&SchemaError{SchemaField: "minItems"},
+				&SchemaError{SchemaField: "pattern", reversePath: []string{"0"}},
+			},
+			{
+				&SchemaError{SchemaField: "maxItems"},
+				&SchemaError{SchemaField: "pattern", reversePath: []string{"0"}},
+				&SchemaError{SchemaField: "pattern", reversePath: []string{"1"}},
+				&SchemaError{SchemaField: "pattern", reversePath: []string{"2"}},
+			},
+		},
+	},
+	{
+		Title: "ARRAY: object",
+		Schema: NewArraySchema().
+			WithItems(NewObjectSchema().
+				WithProperties(map[string]*Schema{
+					"key1": NewStringSchema(),
+					"key2": NewIntegerSchema(),
+				}),
+			),
+		Values: []interface{}{
+			[]interface{}{
+				map[string]interface{}{
+					"key1": 100, // not a string
+					"key2": "not an integer",
+				},
+			},
+		},
+		ExpectedErrors: []MultiError{
+			{
+				&SchemaError{SchemaField: "type", reversePath: []string{"key1", "0"}},
+				&SchemaError{SchemaField: "type", reversePath: []string{"key2", "0"}},
+			},
+		},
+	},
+	{
+		Title: "OBJECT",
+		Schema: NewObjectSchema().
+			WithProperties(map[string]*Schema{
+				"key1": NewStringSchema(),
+				"key2": NewIntegerSchema(),
+				"key3": NewArraySchema().
+					WithItems(NewStringSchema().
+						WithPattern("^[abc]+$")),
+			}),
+		Values: []interface{}{
+			map[string]interface{}{
+				"key1": 100, // not a string
+				"key2": "not an integer",
+				"key3": []interface{}{"abc", "def"},
+			},
+		},
+		ExpectedErrors: []MultiError{
+			{
+				&SchemaError{SchemaField: "type", reversePath: []string{"key1"}},
+				&SchemaError{SchemaField: "type", reversePath: []string{"key2"}},
+				&SchemaError{SchemaField: "pattern", reversePath: []string{"1", "key3"}},
+			},
+		},
+	},
+}
diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go
new file mode 100644
index 0000000..fac9bf5
--- /dev/null
+++ b/openapi3filter/validate_readonly_test.go
@@ -0,0 +1,88 @@
+package openapi3filter
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/stretchr/testify/require"
+)
+
+func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) {
+	const spec = `{
+  "openapi": "3.0.3",
+  "info": {
+    "version": "1.0.0",
+    "title": "title",
+    "description": "desc",
+    "contact": {
+      "email": "email"
+    }
+  },
+  "paths": {
+    "/accounts": {
+      "post": {
+        "description": "Create a new account",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "required": ["_id"],
+                "properties": {
+                  "_id": {
+                    "type": "string",
+                    "description": "Unique identifier for this object.",
+                    "pattern": "[0-9a-v]+$",
+                    "minLength": 20,
+                    "maxLength": 20,
+                    "readOnly": true
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "Successfully created a new account"
+          },
+          "400": {
+            "description": "The server could not understand the request due to invalid syntax",
+          }
+        }
+      }
+    }
+  }
+}
+`
+
+	type Request struct {
+		ID string `json:"_id"`
+	}
+
+	sl := openapi3.NewSwaggerLoader()
+	l, err := sl.LoadSwaggerFromData([]byte(spec))
+	require.NoError(t, err)
+	router := NewRouter().WithSwagger(l)
+
+	b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"})
+	require.NoError(t, err)
+
+	httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b))
+	require.NoError(t, err)
+	httpReq.Header.Add("Content-Type", "application/json")
+
+	route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL)
+	require.NoError(t, err)
+
+	err = ValidateRequest(sl.Context, &RequestValidationInput{
+		Request:    httpReq,
+		PathParams: pathParams,
+		Route:      route,
+	})
+	require.NoError(t, err)
+}
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
index 737ceeb..0a28ab6 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -74,6 +74,9 @@ func getValidationTests(t *testing.T) []*validationTest {
 	unsupportedContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
 	unsupportedContentType.Header.Set("Content-Type", "text/plain")
 
+	unsupportedHeaderValue := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
+	unsupportedHeaderValue.Header.Set("x-environment", "watdis")
+
 	return []*validationTest{
 		//
 		// Basics
@@ -270,10 +273,43 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 		},
 
+		//
+		// Request header params
+		//
+		{
+			name: "error - invalid enum value for header string parameter",
+			args: validationArgs{
+				r: unsupportedHeaderValue,
+			},
+			wantErrParam:        "x-environment",
+			wantErrParamIn:      "header",
+			wantErrSchemaReason: "JSON value is not one of the allowed values",
+			wantErrSchemaPath:   "/",
+			wantErrSchemaValue:  "watdis",
+			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
+				Title:  "JSON value is not one of the allowed values",
+				Detail: "Value 'watdis' at / must be one of: demo, prod",
+				Source: &ValidationErrorSource{Parameter: "x-environment"}},
+		},
+
 		//
 		// Request bodies
 		//
 
+		{
+			name: "error - invalid enum value for header object attribute",
+			args: validationArgs{
+				r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)),
+			},
+			wantErrReason:       "doesn't match the schema",
+			wantErrSchemaReason: "JSON value is not one of the allowed values",
+			wantErrSchemaValue:  "watdis",
+			wantErrSchemaPath:   "/status",
+			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
+				Title:  "JSON value is not one of the allowed values",
+				Detail: "Value 'watdis' at /status must be one of: available, pending, sold",
+				Source: &ValidationErrorSource{Pointer: "/status"}},
+		},
 		{
 			name: "error - missing required object attribute",
 			args: validationArgs{