Repository: getkin/kin-openapi
Pull request #665 (2022-11-15 14:21:24)

fix: query param pattern
This pull request fixes issue https://github.com/getkin/kin-openapi/issues/641


Patch:
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index 53b4b40..8c08205 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -485,6 +485,16 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
 	for i, v := range schema.Value.AllOf {
 		schema.Value.AllOf[i] = ToV3SchemaRef(v)
 	}
+	if val, ok := schema.Value.Extensions["x-nullable"]; ok {
+		var nullable bool
+
+		if err := json.Unmarshal(val.(json.RawMessage), &nullable); err == nil {
+			schema.Value.Nullable = nullable
+		}
+
+		delete(schema.Value.Extensions, "x-nullable")
+	}
+
 	return schema
 }
 
@@ -824,6 +834,11 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components
 	for i, v := range schema.Value.AllOf {
 		schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components)
 	}
+	if schema.Value.Nullable {
+		schema.Value.Nullable = false
+		schema.Value.Extensions["x-nullable"] = true
+	}
+
 	return schema, nil
 }
 
diff --git a/openapi3/schema.go b/openapi3/schema.go
index e5cded8..081873e 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -861,10 +861,11 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
 		}
 	}
 	return &SchemaError{
-		Value:       value,
-		Schema:      schema,
-		SchemaField: "type",
-		Reason:      fmt.Sprintf("unhandled value of type %T", value),
+		Value:                 value,
+		Schema:                schema,
+		SchemaField:           "type",
+		Reason:                fmt.Sprintf("unhandled value of type %T", value),
+		customizeMessageError: settings.customizeMessageError,
 	}
 }
 
@@ -879,10 +880,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 			return errSchema
 		}
 		return &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "enum",
-			Reason:      "value is not one of the allowed values",
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "enum",
+			Reason:                fmt.Sprintf("value %q is not one of the allowed values", value),
+			customizeMessageError: settings.customizeMessageError,
 		}
 	}
 
@@ -896,9 +898,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 				return errSchema
 			}
 			return &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "not",
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "not",
+				customizeMessageError: settings.customizeMessageError,
 			}
 		}
 	}
@@ -961,9 +964,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 				return errSchema
 			}
 			e := &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "oneOf",
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "oneOf",
+				customizeMessageError: settings.customizeMessageError,
 			}
 			if ok > 1 {
 				e.Origin = ErrOneOfConflict
@@ -1005,9 +1009,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 				return errSchema
 			}
 			return &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "anyOf",
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "anyOf",
+				customizeMessageError: settings.customizeMessageError,
 			}
 		}
 
@@ -1024,10 +1029,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 				return errSchema
 			}
 			return &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "allOf",
-				Origin:      err,
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "allOf",
+				Origin:                err,
+				customizeMessageError: settings.customizeMessageError,
 			}
 		}
 	}
@@ -1042,10 +1048,11 @@ func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err err
 		return errSchema
 	}
 	return &SchemaError{
-		Value:       nil,
-		Schema:      schema,
-		SchemaField: "nullable",
-		Reason:      "Value is not nullable",
+		Value:                 nil,
+		Schema:                schema,
+		SchemaField:           "nullable",
+		Reason:                "Value is not nullable",
+		customizeMessageError: settings.customizeMessageError,
 	}
 }
 
@@ -1075,10 +1082,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 				return errSchema
 			}
 			err := &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "type",
-				Reason:      "Value must be an integer",
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "type",
+				Reason:                fmt.Sprintf("value \"%g\" must be an integer", value),
+				customizeMessageError: settings.customizeMessageError,
 			}
 			if !settings.multiError {
 				return err
@@ -1110,10 +1118,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 				return errSchema
 			}
 			err := &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "format",
-				Reason:      fmt.Sprintf("number must be an %s", schema.Format),
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "format",
+				Reason:                fmt.Sprintf("number must be an %s", schema.Format),
+				customizeMessageError: settings.customizeMessageError,
 			}
 			if !settings.multiError {
 				return err
@@ -1128,10 +1137,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "exclusiveMinimum",
-			Reason:      fmt.Sprintf("number must be more than %g", *schema.Min),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "exclusiveMinimum",
+			Reason:                fmt.Sprintf("number must be more than %g", *schema.Min),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1145,10 +1155,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "exclusiveMaximum",
-			Reason:      fmt.Sprintf("number must be less than %g", *schema.Max),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "exclusiveMaximum",
+			Reason:                fmt.Sprintf("number must be less than %g", *schema.Max),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1162,10 +1173,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "minimum",
-			Reason:      fmt.Sprintf("number must be at least %g", *v),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "minimum",
+			Reason:                fmt.Sprintf("number must be at least %g", *v),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1179,10 +1191,11 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "maximum",
-			Reason:      fmt.Sprintf("number must be at most %g", *v),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "maximum",
+			Reason:                fmt.Sprintf("number must be at most %g", *v),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1199,9 +1212,10 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 				return errSchema
 			}
 			err := &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "multipleOf",
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "multipleOf",
+				customizeMessageError: settings.customizeMessageError,
 			}
 			if !settings.multiError {
 				return err
@@ -1247,10 +1261,11 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 				return errSchema
 			}
 			err := &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "minLength",
-				Reason:      fmt.Sprintf("minimum string length is %d", minLength),
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "minLength",
+				Reason:                fmt.Sprintf("minimum string length is %d", minLength),
+				customizeMessageError: settings.customizeMessageError,
 			}
 			if !settings.multiError {
 				return err
@@ -1262,10 +1277,11 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 				return errSchema
 			}
 			err := &SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "maxLength",
-				Reason:      fmt.Sprintf("maximum string length is %d", *maxLength),
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "maxLength",
+				Reason:                fmt.Sprintf("maximum string length is %d", *maxLength),
+				customizeMessageError: settings.customizeMessageError,
 			}
 			if !settings.multiError {
 				return err
@@ -1286,10 +1302,11 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 	}
 	if cp := schema.compiledPattern; cp != nil && !cp.MatchString(value) {
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "pattern",
-			Reason:      fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "pattern",
+			Reason:                fmt.Sprintf(`string %q doesn't match the regular expression "%s"`, value, schema.Pattern),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1298,29 +1315,32 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 	}
 
 	// "format"
-	var formatErr string
+	var formatStrErr string
+	var formatErr error
 	if format := schema.Format; format != "" {
 		if f, ok := SchemaStringFormats[format]; ok {
 			switch {
 			case f.regexp != nil && f.callback == nil:
 				if cp := f.regexp; !cp.MatchString(value) {
-					formatErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
+					formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
 				}
 			case f.regexp == nil && f.callback != nil:
 				if err := f.callback(value); err != nil {
-					formatErr = err.Error()
+					formatErr = err
 				}
 			default:
-				formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format)
+				formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format)
 			}
 		}
 	}
-	if formatErr != "" {
+	if formatStrErr != "" || formatErr != nil {
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "format",
-			Reason:      formatErr,
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "format",
+			Reason:                formatStrErr,
+			Origin:                formatErr,
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1356,10 +1376,11 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "minItems",
-			Reason:      fmt.Sprintf("minimum number of items is %d", v),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "minItems",
+			Reason:                fmt.Sprintf("minimum number of items is %d", v),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1373,10 +1394,11 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "maxItems",
-			Reason:      fmt.Sprintf("maximum number of items is %d", *v),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "maxItems",
+			Reason:                fmt.Sprintf("maximum number of items is %d", *v),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1393,10 +1415,11 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "uniqueItems",
-			Reason:      "duplicate items found",
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "uniqueItems",
+			Reason:                "duplicate items found",
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1484,10 +1507,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "minProperties",
-			Reason:      fmt.Sprintf("there must be at least %d properties", v),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "minProperties",
+			Reason:                fmt.Sprintf("there must be at least %d properties", v),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1501,10 +1525,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "maxProperties",
-			Reason:      fmt.Sprintf("there must be at most %d properties", *v),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "maxProperties",
+			Reason:                fmt.Sprintf("there must be at most %d properties", *v),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1572,10 +1597,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
 			return errSchema
 		}
 		err := &SchemaError{
-			Value:       value,
-			Schema:      schema,
-			SchemaField: "properties",
-			Reason:      fmt.Sprintf("property %q is unsupported", k),
+			Value:                 value,
+			Schema:                schema,
+			SchemaField:           "properties",
+			Reason:                fmt.Sprintf("property %q is unsupported", k),
+			customizeMessageError: settings.customizeMessageError,
 		}
 		if !settings.multiError {
 			return err
@@ -1596,10 +1622,11 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
 				return errSchema
 			}
 			err := markSchemaErrorKey(&SchemaError{
-				Value:       value,
-				Schema:      schema,
-				SchemaField: "required",
-				Reason:      fmt.Sprintf("property %q is missing", k),
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "required",
+				Reason:                fmt.Sprintf("property %q is missing", k),
+				customizeMessageError: settings.customizeMessageError,
 			}, k)
 			if !settings.multiError {
 				return err
@@ -1620,10 +1647,11 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, typ strin
 		return errSchema
 	}
 	return &SchemaError{
-		Value:       typ,
-		Schema:      schema,
-		SchemaField: "type",
-		Reason:      "Field must be set to " + schema.Type + " or not be present",
+		Value:                 typ,
+		Schema:                schema,
+		SchemaField:           "type",
+		Reason:                fmt.Sprintf("field must be set to %s or not be present", schema.Type),
+		customizeMessageError: settings.customizeMessageError,
 	}
 }
 
@@ -1639,12 +1667,13 @@ func (schema *Schema) compilePattern() (err error) {
 }
 
 type SchemaError struct {
-	Value       interface{}
-	reversePath []string
-	Schema      *Schema
-	SchemaField string
-	Reason      string
-	Origin      error
+	Value                 interface{}
+	reversePath           []string
+	Schema                *Schema
+	SchemaField           string
+	Reason                string
+	Origin                error
+	customizeMessageError func(err *SchemaError) string
 }
 
 var _ interface{ Unwrap() error } = SchemaError{}
@@ -1687,11 +1716,14 @@ func (err *SchemaError) JSONPointer() []string {
 }
 
 func (err *SchemaError) Error() string {
-	if err.Origin != nil {
-		return err.Origin.Error()
+	if err.customizeMessageError != nil {
+		if msg := err.customizeMessageError(err); msg != "" {
+			return msg
+		}
 	}
 
 	buf := bytes.NewBuffer(make([]byte, 0, 256))
+
 	if len(err.reversePath) > 0 {
 		buf.WriteString(`Error at "`)
 		reversePath := err.reversePath
@@ -1701,6 +1733,13 @@ func (err *SchemaError) Error() string {
 		}
 		buf.WriteString(`": `)
 	}
+
+	if err.Origin != nil {
+		buf.WriteString(err.Origin.Error())
+
+		return buf.String()
+	}
+
 	reason := err.Reason
 	if reason == "" {
 		buf.WriteString(`Doesn't match schema "`)
@@ -1709,6 +1748,7 @@ func (err *SchemaError) Error() string {
 	} else {
 		buf.WriteString(reason)
 	}
+
 	if !SchemaErrorDetailsDisabled {
 		buf.WriteString("\nSchema:\n  ")
 		encoder := json.NewEncoder(buf)
@@ -1721,6 +1761,7 @@ func (err *SchemaError) Error() string {
 			panic(err)
 		}
 	}
+
 	return buf.String()
 }
 
diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go
index 854ae84..5a28c8d 100644
--- a/openapi3/schema_validation_settings.go
+++ b/openapi3/schema_validation_settings.go
@@ -16,6 +16,8 @@ type schemaValidationSettings struct {
 
 	onceSettingDefaults sync.Once
 	defaultsSet         func()
+
+	customizeMessageError func(err *SchemaError) string
 }
 
 // FailFast returns schema validation errors quicker.
@@ -50,6 +52,12 @@ func DefaultsSet(f func()) SchemaValidationOption {
 	return func(s *schemaValidationSettings) { s.defaultsSet = f }
 }
 
+// SetSchemaErrorMessageCustomizer allows to override the schema error message.
+// If the passed function returns an empty string, it returns to the previous Error() implementation.
+func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.customizeMessageError = f }
+}
+
 func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings {
 	settings := &schemaValidationSettings{}
 	for _, opt := range opts {
diff --git a/openapi3filter/options.go b/openapi3filter/options.go
index 1622339..14c35d5 100644
--- a/openapi3filter/options.go
+++ b/openapi3filter/options.go
@@ -1,5 +1,7 @@
 package openapi3filter
 
+import "github.com/getkin/kin-openapi/openapi3"
+
 // DefaultOptions do not set an AuthenticationFunc.
 // A spec with security schemes defined will not pass validation
 // unless an AuthenticationFunc is defined.
@@ -21,4 +23,19 @@ type Options struct {
 
 	// See NoopAuthenticationFunc
 	AuthenticationFunc AuthenticationFunc
+
+	// Indicates whether default values are set in the
+	// request. If true, then they are not set
+	SkipSettingDefaults bool
+
+	customSchemaErrorFunc CustomSchemaErrorFunc
+}
+
+// CustomSchemaErrorFunc allows for custom the schema error message.
+type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string
+
+// WithCustomSchemaErrorFunc sets a function to override the schema error message.
+// If the passed function returns an empty string, it returns to the previous Error() implementation.
+func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc) {
+	o.customSchemaErrorFunc = f
 }
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index ca81944..59c383d 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -334,6 +334,9 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho
 	case *pathParamDecoder:
 		_, found = vDecoder.pathParams[param]
 	case *urlValuesDecoder:
+		if schema.Value.Pattern != "" {
+			return dec.DecodePrimitive(param, sm, schema)
+		}
 		_, found = vDecoder.values[param]
 	case *headerParamDecoder:
 		_, found = vDecoder.header[param]
@@ -500,6 +503,10 @@ func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.Serializat
 		// HTTP request does not contain a value of the target query parameter.
 		return nil, ok, nil
 	}
+
+	if schema.Value.Type == "" && schema.Value.Pattern != "" {
+		return values[0], ok, nil
+	}
 	val, err := parsePrimitive(values[0], schema)
 	return val, ok, err
 }
@@ -526,10 +533,92 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM
 		}
 		values = strings.Split(values[0], delim)
 	}
-	val, err := parseArray(values, schema)
+	val, err := d.parseArray(values, sm, schema)
 	return val, ok, err
 }
 
+// parseArray returns an array that contains items from a raw array.
+// Every item is parsed as a primitive value.
+// The function returns an error when an error happened while parse array's items.
+func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]interface{}, error) {
+	var value []interface{}
+
+	for i, v := range raw {
+		item, err := d.parseValue(v, schemaRef.Value.Items)
+		if err != nil {
+			if v, ok := err.(*ParseError); ok {
+				return nil, &ParseError{path: []interface{}{i}, Cause: v}
+			}
+			return nil, fmt.Errorf("item %d: %w", i, err)
+		}
+
+		// If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive
+		// values and some are nil values.
+		if item == nil {
+			return nil, nil
+		}
+		value = append(value, item)
+	}
+	return value, nil
+}
+
+func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) {
+	if len(schema.Value.AllOf) > 0 {
+		var value interface{}
+		var err error
+		for _, sr := range schema.Value.AllOf {
+			value, err = d.parseValue(v, sr)
+			if value == nil || err != nil {
+				break
+			}
+		}
+		return value, err
+	}
+
+	if len(schema.Value.AnyOf) > 0 {
+		var value interface{}
+		var err error
+		for _, sr := range schema.Value.AnyOf {
+			value, err = d.parseValue(v, sr)
+			if err == nil {
+				return value, nil
+			}
+		}
+
+		return nil, err
+	}
+
+	if len(schema.Value.OneOf) > 0 {
+		isMatched := 0
+		var value interface{}
+		var err error
+		for _, sr := range schema.Value.OneOf {
+			result, err := d.parseValue(v, sr)
+			if err == nil {
+				value = result
+				isMatched++
+			}
+		}
+		if isMatched == 1 {
+			return value, nil
+		} else if isMatched > 1 {
+			return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
+		} else if isMatched == 0 {
+			return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
+		}
+
+		return nil, err
+	}
+
+	if schema.Value.Not != nil {
+		// TODO(decode not): handle decoding "not" JSON Schema
+		return nil, errors.New("not implemented: decoding 'not'")
+	}
+
+	return parsePrimitive(v, schema)
+
+}
+
 func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
 	var propsFn func(url.Values) (map[string]string, error)
 	switch sm.Style {
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index 4b0bd34..4acb9ff 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -178,6 +178,9 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param
 		opts = make([]openapi3.SchemaValidationOption, 0, 1)
 		opts = append(opts, openapi3.MultiErrors())
 	}
+	if options.customSchemaErrorFunc != nil {
+		opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
+	}
 	if err = schema.VisitJSON(value, opts...); err != nil {
 		return &RequestError{Input: input, Parameter: parameter, Err: err}
 	}
@@ -258,10 +261,15 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
 	defaultsSet := false
 	opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here
 	opts = append(opts, openapi3.VisitAsRequest())
-	opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
+	if !options.SkipSettingDefaults {
+		opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
+	}
 	if options.MultiError {
 		opts = append(opts, openapi3.MultiErrors())
 	}
+	if options.customSchemaErrorFunc != nil {
+		opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
+	}
 
 	// Validate JSON with the schema
 	if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
@@ -285,6 +293,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
 		}
 		// Put the data back into the input
 		req.Body = ioutil.NopCloser(bytes.NewReader(data))
+		req.ContentLength = int64(len(data))
 	}
 
 	return nil
diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go
index e90b5d6..abcbb4e 100644
--- a/openapi3filter/validate_response.go
+++ b/openapi3filter/validate_response.go
@@ -66,6 +66,9 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
 	if options.MultiError {
 		opts = append(opts, openapi3.MultiErrors())
 	}
+	if options.customSchemaErrorFunc != nil {
+		opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
+	}
 
 	headers := make([]string, 0, len(response.Headers))
 	for k := range response.Headers {


Test patch:
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index 9edce54..3177721 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -89,7 +89,8 @@ const exampleV2 = `
 			"additionalProperties": true,
 			"properties": {
 				"foo": {
-					"type": "string"
+					"type": "string",
+					"x-nullable": true
 				},
 				"quux": {
 					"$ref": "#/definitions/ItemExtension"
@@ -463,7 +464,8 @@ const exampleV3 = `
 				"additionalProperties": true,
 				"properties": {
 					"foo": {
-						"type": "string"
+						"type": "string",
+						"nullable": true
 					},
 					"quux": {
 						"$ref": "#/components/schemas/ItemExtension"
diff --git a/openapi3/issue136_test.go b/openapi3/issue136_test.go
index 36c8b85..b5e9eeb 100644
--- a/openapi3/issue136_test.go
+++ b/openapi3/issue136_test.go
@@ -31,7 +31,7 @@ components:
 		},
 		{
 			dflt: `1`,
-			err:  "invalid components: invalid schema default: Field must be set to string or not be present",
+			err:  "invalid components: invalid schema default: field must be set to string or not be present",
 		},
 	} {
 		t.Run(testcase.dflt, func(t *testing.T) {
diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go
index 5cceb8c..3899fab 100644
--- a/openapi3/schema_formats_test.go
+++ b/openapi3/schema_formats_test.go
@@ -2,8 +2,10 @@ package openapi3
 
 import (
 	"context"
+	"errors"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -58,3 +60,48 @@ func TestIssue430(t *testing.T) {
 		}
 	}
 }
+
+func TestFormatCallback_WrapError(t *testing.T) {
+	var errSomething = errors.New("something error")
+
+	DefineStringFormatCallback("foobar", func(value string) error {
+		return errSomething
+	})
+
+	s := &Schema{Format: "foobar"}
+	err := s.VisitJSONString("blablabla")
+
+	assert.ErrorIs(t, err, errSomething)
+
+	delete(SchemaStringFormats, "foobar")
+}
+
+func TestReversePathInMessageSchemaError(t *testing.T) {
+	DefineIPv4Format()
+
+	SchemaErrorDetailsDisabled = true
+
+	const spc = `
+components:
+  schemas:
+    Something:
+      type: object
+      properties:
+        ip:
+          type: string
+          format: ipv4
+`
+	l := NewLoader()
+
+	doc, err := l.LoadFromData([]byte(spc))
+	require.NoError(t, err)
+
+	err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{
+		`ip`: `123.0.0.11111`,
+	})
+
+	require.EqualError(t, err, `Error at "/ip": Not an IP address`)
+
+	delete(SchemaStringFormats, "ipv4")
+	SchemaErrorDetailsDisabled = false
+}
diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go
index 6ab6b63..56d1d45 100644
--- a/openapi3/schema_issue289_test.go
+++ b/openapi3/schema_issue289_test.go
@@ -35,5 +35,5 @@ openapi: "3.0.1"
 		"name":    "kin-openapi",
 		"address": "127.0.0.1",
 	})
-	require.EqualError(t, err, ErrOneOfConflict.Error())
+	require.ErrorIs(t, err, ErrOneOfConflict)
 }
diff --git a/openapi3/schema_validation_settings_test.go b/openapi3/schema_validation_settings_test.go
new file mode 100644
index 0000000..db52d3b
--- /dev/null
+++ b/openapi3/schema_validation_settings_test.go
@@ -0,0 +1,36 @@
+package openapi3_test
+
+import (
+	"fmt"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+func ExampleSetSchemaErrorMessageCustomizer() {
+	loader := openapi3.NewLoader()
+	spc := `
+components:
+  schemas:
+    Something:
+      type: object
+      properties:
+        field:
+          title: Some field
+          type: string
+`[1:]
+
+	doc, err := loader.LoadFromData([]byte(spc))
+	if err != nil {
+		panic(err)
+	}
+
+	opt := openapi3.SetSchemaErrorMessageCustomizer(func(err *openapi3.SchemaError) string {
+		return fmt.Sprintf(`field "%s" should be string`, err.Schema.Title)
+	})
+
+	err = doc.Components.Schemas["Something"].Value.Properties["field"].Value.VisitJSON(123, opt)
+
+	fmt.Println(err.Error())
+
+	// Output: field "Some field" should be string
+}
diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go
index 8db8620..8b2b99d 100644
--- a/openapi3filter/issue201_test.go
+++ b/openapi3filter/issue201_test.go
@@ -94,7 +94,7 @@ paths:
 		},
 
 		"invalid required header": {
-			err: `response header "X-Blup" doesn't match the schema: string doesn't match the regular expression "^blup$"`,
+			err: `response header "X-Blup" doesn't match the schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`,
 			headers: map[string]string{
 				"X-Blip": "blip",
 				"x-blop": "blop",
diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go
new file mode 100644
index 0000000..d9e5bae
--- /dev/null
+++ b/openapi3filter/issue625_test.go
@@ -0,0 +1,123 @@
+package openapi3filter_test
+
+import (
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIssue625(t *testing.T) {
+
+	anyOfArraySpec := `
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Sample API
+paths:
+ /items:
+  get:
+    description: Returns a list of stuff
+    parameters:
+    - description: test object
+      explode: false
+      in: query
+      name: test
+      required: false
+      schema:
+        type: array
+        items:
+         anyOf:
+          - type: integer
+          - type: boolean
+    responses:
+      '200':
+        description: Successful response
+`[1:]
+
+	oneOfArraySpec := strings.ReplaceAll(anyOfArraySpec, "anyOf", "oneOf")
+
+	allOfArraySpec := strings.ReplaceAll(strings.ReplaceAll(anyOfArraySpec, "anyOf", "allOf"),
+		"type: boolean", "type: number")
+
+	tests := []struct {
+		name   string
+		spec   string
+		req    string
+		errStr string
+	}{
+		{
+			name: "success anyof object array",
+			spec: anyOfArraySpec,
+			req:  "/items?test=3,7",
+		},
+		{
+			name:   "failed anyof object array",
+			spec:   anyOfArraySpec,
+			req:    "/items?test=s1,s2",
+			errStr: `parameter "test" in query has an error: path 0: value s1: an invalid boolean: invalid syntax`,
+		},
+
+		{
+			name: "success allof object array",
+			spec: allOfArraySpec,
+			req:  `/items?test=1,3`,
+		},
+		{
+			name:   "failed allof object array",
+			spec:   allOfArraySpec,
+			req:    `/items?test=1.2,3.1`,
+			errStr: `parameter "test" in query has an error: Error at "/0": value "1.2" must be an integer`,
+		},
+		{
+			name: "success oneof object array",
+			spec: oneOfArraySpec,
+			req:  `/items?test=true,3`,
+		},
+		{
+			name:   "faled oneof object array",
+			spec:   oneOfArraySpec,
+			req:    `/items?test="val1","val2"`,
+			errStr: `parameter "test" in query has an error: item 0: decoding oneOf failed: 0 schemas matched`,
+		},
+	}
+
+	for _, testcase := range tests {
+		t.Run(testcase.name, func(t *testing.T) {
+			loader := openapi3.NewLoader()
+			ctx := loader.Context
+
+			doc, err := loader.LoadFromData([]byte(testcase.spec))
+			require.NoError(t, err)
+
+			err = doc.Validate(ctx)
+			require.NoError(t, err)
+
+			router, err := gorillamux.NewRouter(doc)
+			require.NoError(t, err)
+			httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil)
+			require.NoError(t, err)
+
+			route, pathParams, err := router.FindRoute(httpReq)
+			require.NoError(t, err)
+
+			requestValidationInput := &openapi3filter.RequestValidationInput{
+				Request:    httpReq,
+				PathParams: pathParams,
+				Route:      route,
+			}
+			err = openapi3filter.ValidateRequest(ctx, requestValidationInput)
+			if testcase.errStr == "" {
+				require.NoError(t, err)
+			} else {
+				require.Contains(t, err.Error(), testcase.errStr)
+			}
+		},
+		)
+	}
+}
diff --git a/openapi3filter/issue639_test.go b/openapi3filter/issue639_test.go
new file mode 100644
index 0000000..2caf1bd
--- /dev/null
+++ b/openapi3filter/issue639_test.go
@@ -0,0 +1,100 @@
+package openapi3filter
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIssue639(t *testing.T) {
+	loader := openapi3.NewLoader()
+	ctx := loader.Context
+	spec := `
+    openapi: 3.0.0
+    info:
+     version: 1.0.0
+     title: Sample API
+    paths:
+     /items:
+      put:
+        requestBody:
+         content:
+          application/json:
+            schema:
+              properties:
+                testWithdefault:
+                  default: false
+                  type: boolean
+                testNoDefault:
+                  type: boolean
+              type: object
+        responses:
+          '200':
+           description: Successful respons
+`[1:]
+
+	doc, err := loader.LoadFromData([]byte(spec))
+	require.NoError(t, err)
+
+	err = doc.Validate(ctx)
+	require.NoError(t, err)
+
+	router, err := gorillamux.NewRouter(doc)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name               string
+		options            *Options
+		expectedDefaultVal interface{}
+	}{
+		{
+			name: "no defaults are added to requests",
+			options: &Options{
+				SkipSettingDefaults: true,
+			},
+			expectedDefaultVal: nil,
+		},
+
+		{
+			name:               "defaults are added to requests",
+			expectedDefaultVal: false,
+		},
+	}
+
+	for _, testcase := range tests {
+		t.Run(testcase.name, func(t *testing.T) {
+			body := "{\"testNoDefault\": true}"
+			httpReq, err := http.NewRequest(http.MethodPut, "/items", strings.NewReader(body))
+			require.NoError(t, err)
+			httpReq.Header.Set("Content-Type", "application/json")
+			require.NoError(t, err)
+
+			route, pathParams, err := router.FindRoute(httpReq)
+			require.NoError(t, err)
+
+			requestValidationInput := &RequestValidationInput{
+				Request:    httpReq,
+				PathParams: pathParams,
+				Route:      route,
+				Options:    testcase.options,
+			}
+			err = ValidateRequest(ctx, requestValidationInput)
+			require.NoError(t, err)
+			bodyAfterValidation, err := ioutil.ReadAll(httpReq.Body)
+			require.NoError(t, err)
+
+			raw := map[string]interface{}{}
+			err = json.Unmarshal(bodyAfterValidation, &raw)
+			require.NoError(t, err)
+			require.Equal(t, testcase.expectedDefaultVal,
+				raw["testWithdefault"], "default value must not be included")
+		})
+	}
+}
diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go
new file mode 100644
index 0000000..3e0bba2
--- /dev/null
+++ b/openapi3filter/issue641_test.go
@@ -0,0 +1,109 @@
+package openapi3filter_test
+
+import (
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIssue641(t *testing.T) {
+
+	anyOfSpec := `
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Sample API
+paths:
+ /items:
+  get:
+    description: Returns a list of stuff
+    parameters:
+    - description: test object
+      explode: false
+      in: query
+      name: test
+      required: false
+      schema:
+        anyOf:
+        - pattern: "^[0-9]{1,4}$"
+        - pattern: "^[0-9]{1,4}$"
+        type: string
+    responses:
+      '200':
+        description: Successful response
+`[1:]
+
+	allOfSpec := strings.ReplaceAll(anyOfSpec, "anyOf", "allOf")
+
+	tests := []struct {
+		name   string
+		spec   string
+		req    string
+		errStr string
+	}{
+
+		{
+			name: "success anyof pattern",
+			spec: anyOfSpec,
+			req:  "/items?test=51",
+		},
+		{
+			name:   "failed anyof pattern",
+			spec:   anyOfSpec,
+			req:    "/items?test=999999",
+			errStr: `parameter "test" in query has an error: Doesn't match schema "anyOf"`,
+		},
+
+		{
+			name: "success allof pattern",
+			spec: allOfSpec,
+			req:  `/items?test=51`,
+		},
+		{
+			name:   "failed allof pattern",
+			spec:   allOfSpec,
+			req:    `/items?test=999999`,
+			errStr: `parameter "test" in query has an error: string doesn't match the regular expression`,
+		},
+	}
+
+	for _, testcase := range tests {
+		t.Run(testcase.name, func(t *testing.T) {
+			loader := openapi3.NewLoader()
+			ctx := loader.Context
+
+			doc, err := loader.LoadFromData([]byte(testcase.spec))
+			require.NoError(t, err)
+
+			err = doc.Validate(ctx)
+			require.NoError(t, err)
+
+			router, err := gorillamux.NewRouter(doc)
+			require.NoError(t, err)
+			httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil)
+			require.NoError(t, err)
+
+			route, pathParams, err := router.FindRoute(httpReq)
+			require.NoError(t, err)
+
+			requestValidationInput := &openapi3filter.RequestValidationInput{
+				Request:    httpReq,
+				PathParams: pathParams,
+				Route:      route,
+			}
+			err = openapi3filter.ValidateRequest(ctx, requestValidationInput)
+			if testcase.errStr == "" {
+				require.NoError(t, err)
+			} else {
+				require.Contains(t, err.Error(), testcase.errStr)
+			}
+		},
+		)
+	}
+}
diff --git a/openapi3filter/options_test.go b/openapi3filter/options_test.go
new file mode 100644
index 0000000..1273711
--- /dev/null
+++ b/openapi3filter/options_test.go
@@ -0,0 +1,83 @@
+package openapi3filter_test
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func ExampleOptions_WithCustomSchemaErrorFunc() {
+	const spec = `
+openapi: 3.0.0
+info:
+  title: 'Validator'
+  version: 0.0.1
+paths:
+  /some:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                field:
+                  title: Some field
+                  type: integer
+      responses:
+        '200':
+          description: Created
+`
+
+	loader := openapi3.NewLoader()
+	doc, err := loader.LoadFromData([]byte(spec))
+	if err != nil {
+		panic(err)
+	}
+
+	err = doc.Validate(loader.Context)
+	if err != nil {
+		panic(err)
+	}
+
+	router, err := gorillamux.NewRouter(doc)
+	if err != nil {
+		panic(err)
+	}
+
+	opts := &openapi3filter.Options{}
+
+	opts.WithCustomSchemaErrorFunc(func(err *openapi3.SchemaError) string {
+		return fmt.Sprintf(`field "%s" must be an integer`, err.Schema.Title)
+	})
+
+	req, err := http.NewRequest(http.MethodPost, "/some", strings.NewReader(`{"field":"not integer"}`))
+	if err != nil {
+		panic(err)
+	}
+
+	req.Header.Add("Content-Type", "application/json")
+
+	route, pathParams, err := router.FindRoute(req)
+	if err != nil {
+		panic(err)
+	}
+
+	validationInput := &openapi3filter.RequestValidationInput{
+		Request:    req,
+		PathParams: pathParams,
+		Route:      route,
+		Options:    opts,
+	}
+	err = openapi3filter.ValidateRequest(context.Background(), validationInput)
+
+	fmt.Println(err.Error())
+
+	// Output: request body has an error: doesn't match the schema: field "Some field" must be an integer
+}
diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go
index 552e20e..0ee48ad 100644
--- a/openapi3filter/unpack_errors_test.go
+++ b/openapi3filter/unpack_errors_test.go
@@ -81,7 +81,7 @@ func Example() {
 	// Output:
 	// ===== Start New Error =====
 	// @body.name:
-	// 	Error at "/name": Field must be set to string or not be present
+	// 	Error at "/name": field must be set to string or not be present
 	// Schema:
 	//   {
 	//     "example": "doggie",
@@ -93,7 +93,7 @@ func Example() {
 	//
 	// ===== Start New Error =====
 	// @body.status:
-	// 	Error at "/status": value is not one of the allowed values
+	// 	Error at "/status": value "invalidStatus" is not one of the allowed values
 	// Schema:
 	//   {
 	//     "description": "pet status in the store",
diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go
index 957b692..450ee59 100644
--- a/openapi3filter/validate_request_test.go
+++ b/openapi3filter/validate_request_test.go
@@ -59,6 +59,9 @@ paths:
               properties:
                 subCategory:
                   type: string
+                category:
+                  type: string
+                  default: Sweets
       responses:
         '201':
           description: Created
@@ -95,6 +98,7 @@ components:
 
 	type testRequestBody struct {
 		SubCategory string `json:"subCategory"`
+		Category    string `json:"category,omitempty"`
 	}
 	type args struct {
 		requestBody *testRequestBody
@@ -102,18 +106,30 @@ components:
 		apiKey      string
 	}
 	tests := []struct {
-		name        string
-		args        args
-		expectedErr error
+		name                 string
+		args                 args
+		expectedModification bool
+		expectedErr          error
 	}{
 		{
-			name: "Valid request",
+			name: "Valid request with all fields set",
+			args: args{
+				requestBody: &testRequestBody{SubCategory: "Chocolate", Category: "Food"},
+				url:         "/category?category=cookies",
+				apiKey:      "SomeKey",
+			},
+			expectedModification: false,
+			expectedErr:          nil,
+		},
+		{
+			name: "Valid request without certain fields",
 			args: args{
 				requestBody: &testRequestBody{SubCategory: "Chocolate"},
 				url:         "/category?category=cookies",
 				apiKey:      "SomeKey",
 			},
-			expectedErr: nil,
+			expectedModification: true,
+			expectedErr:          nil,
 		},
 		{
 			name: "Invalid operation params",
@@ -122,7 +138,8 @@ components:
 				url:         "/category?invalidCategory=badCookie",
 				apiKey:      "SomeKey",
 			},
-			expectedErr: &RequestError{},
+			expectedModification: false,
+			expectedErr:          &RequestError{},
 		},
 		{
 			name: "Invalid request body",
@@ -131,7 +148,8 @@ components:
 				url:         "/category?category=cookies",
 				apiKey:      "SomeKey",
 			},
-			expectedErr: &RequestError{},
+			expectedModification: false,
+			expectedErr:          &RequestError{},
 		},
 		{
 			name: "Invalid security",
@@ -140,7 +158,8 @@ components:
 				url:         "/category?category=cookies",
 				apiKey:      "",
 			},
-			expectedErr: &SecurityRequirementsError{},
+			expectedModification: false,
+			expectedErr:          &SecurityRequirementsError{},
 		},
 		{
 			name: "Invalid request body and security",
@@ -149,16 +168,19 @@ components:
 				url:         "/category?category=cookies",
 				apiKey:      "",
 			},
-			expectedErr: &SecurityRequirementsError{},
+			expectedModification: false,
+			expectedErr:          &SecurityRequirementsError{},
 		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
 			var requestBody io.Reader
+			var originalBodySize int
 			if tc.args.requestBody != nil {
 				testingBody, err := json.Marshal(tc.args.requestBody)
 				require.NoError(t, err)
 				requestBody = bytes.NewReader(testingBody)
+				originalBodySize = len(testingBody)
 			}
 			req, err := http.NewRequest(http.MethodPost, tc.args.url, requestBody)
 			require.NoError(t, err)
@@ -180,6 +202,16 @@ components:
 			}
 			err = ValidateRequest(context.Background(), validationInput)
 			assert.IsType(t, tc.expectedErr, err, "ValidateRequest(): error = %v, expectedError %v", err, tc.expectedErr)
+			if tc.expectedErr != nil {
+				return
+			}
+			body, err := io.ReadAll(validationInput.Request.Body)
+			contentLen := int(validationInput.Request.ContentLength)
+			bodySize := len(body)
+			assert.NoError(t, err, "unable to read request body: %v", err)
+			assert.Equal(t, contentLen, bodySize, "expect ContentLength %d to equal body size %d", contentLen, bodySize)
+			bodyModified := originalBodySize != bodySize
+			assert.Equal(t, bodyModified, tc.expectedModification, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification)
 		})
 	}
 }
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
index c1d4630..6fee135 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -244,11 +244,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 			wantErrParam:        "status",
 			wantErrParamIn:      "query",
-			wantErrSchemaReason: "value is not one of the allowed values",
+			wantErrSchemaReason: "value \"available,sold\" is not one of the allowed values",
 			wantErrSchemaPath:   "/0",
 			wantErrSchemaValue:  "available,sold",
 			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title: "value is not one of the allowed values",
+				Title: "value \"available,sold\" 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?
@@ -262,11 +262,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 			wantErrParam:        "status",
 			wantErrParamIn:      "query",
-			wantErrSchemaReason: "value is not one of the allowed values",
+			wantErrSchemaReason: "value \"watdis\" is not one of the allowed values",
 			wantErrSchemaPath:   "/1",
 			wantErrSchemaValue:  "watdis",
 			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title:  "value is not one of the allowed values",
+				Title:  "value \"watdis\" is not one of the allowed values",
 				Detail: "value watdis at /1 must be one of: available, pending, sold",
 				Source: &ValidationErrorSource{Parameter: "status"}},
 		},
@@ -278,11 +278,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 			wantErrParam:        "kind",
 			wantErrParamIn:      "query",
-			wantErrSchemaReason: "value is not one of the allowed values",
+			wantErrSchemaReason: "value \"fish,with,commas\" is not one of the allowed values",
 			wantErrSchemaPath:   "/1",
 			wantErrSchemaValue:  "fish,with,commas",
 			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title:  "value is not one of the allowed values",
+				Title:  "value \"fish,with,commas\" 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"}},
@@ -304,11 +304,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 			wantErrParam:        "x-environment",
 			wantErrParamIn:      "header",
-			wantErrSchemaReason: "value is not one of the allowed values",
+			wantErrSchemaReason: "value \"watdis\" is not one of the allowed values",
 			wantErrSchemaPath:   "/",
 			wantErrSchemaValue:  "watdis",
 			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
-				Title:  "value is not one of the allowed values",
+				Title:  "value \"watdis\" is not one of the allowed values",
 				Detail: "value watdis at / must be one of: demo, prod",
 				Source: &ValidationErrorSource{Parameter: "x-environment"}},
 		},
@@ -323,11 +323,11 @@ func getValidationTests(t *testing.T) []*validationTest {
 				r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)),
 			},
 			wantErrReason:       "doesn't match the schema",
-			wantErrSchemaReason: "value is not one of the allowed values",
+			wantErrSchemaReason: "value \"watdis\" is not one of the allowed values",
 			wantErrSchemaValue:  "watdis",
 			wantErrSchemaPath:   "/status",
 			wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
-				Title:  "value is not one of the allowed values",
+				Title:  "value \"watdis\" is not one of the allowed values",
 				Detail: "value watdis at /status must be one of: available, pending, sold",
 				Source: &ValidationErrorSource{Pointer: "/status"}},
 		},
@@ -379,13 +379,13 @@ func getValidationTests(t *testing.T) []*validationTest {
 					bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)),
 			},
 			wantErrReason:       "doesn't match the schema",
-			wantErrSchemaReason: "Field must be set to array or not be present",
+			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",
+				Title:  "field must be set to array or not be present",
 				Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
 		},
 		{
@@ -659,7 +659,7 @@ func TestValidationHandler_ServeHTTP(t *testing.T) {
 		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))
+		require.Equal(t, "[422][][] field must be set to array or not be present [source pointer=/photoUrls]", string(body))
 	})
 }
 
@@ -701,6 +701,6 @@ func TestValidationHandler_Middleware(t *testing.T) {
 		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))
+		require.Equal(t, "[422][][] field must be set to array or not be present [source pointer=/photoUrls]", string(body))
 	})
 }
diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go
index 33ce98e..2ca3225 100644
--- a/routers/gorillamux/example_test.go
+++ b/routers/gorillamux/example_test.go
@@ -53,7 +53,7 @@ func Example() {
 	err = openapi3filter.ValidateResponse(ctx, responseValidationInput)
 	fmt.Println(err)
 	// Output:
-	// response body doesn't match the schema: Field must be set to string or not be present
+	// response body doesn't match the schema: field must be set to string or not be present
 	// Schema:
 	//   {
 	//     "type": "string"