Repository: getkin/kin-openapi
Pull request #76 (2019-03-20 09:18:46)

Add validation of parameters of all types declared by JSON schema
Proposed fix for issue #69.


Patch:
diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go
index 43d1996..aa7df87 100644
--- a/openapi3/schema_formats.go
+++ b/openapi3/schema_formats.go
@@ -29,5 +29,5 @@ func init() {
 	DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`)
 
 	// 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}(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`)
+	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})?$`)
 }
diff --git a/openapi3filter/param_decoder.go b/openapi3filter/param_decoder.go
index 41b424b..7e9aa02 100644
--- a/openapi3filter/param_decoder.go
+++ b/openapi3filter/param_decoder.go
@@ -10,15 +10,31 @@ import (
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
+const errMsgInvalidSerializationF = "%s parameter %q has an invalid serialization method: style=%q, explode=%v"
+
+// ParseErrorKind describes a kind of ParseError.
+// The type simplifies comparison of errors.
+type ParseErrorKind int
+
 const (
-	errMsgInvalidValue          = "an invalid value"
-	errMsgInvalidSerializationF = "%s parameter %q has an invalid serialization method: style=%q, explode=%v"
+	// KindOther describes an untyped parsing error.
+	KindOther ParseErrorKind = iota
+	// KindInvalidFormat describes an error that happens when a value does not conform a format
+	// that is required by a serialization method.
+	KindInvalidFormat
+	// KindInvalidInt describes an error that happens when a value is an invalid integer.
+	KindInvalidInt
+	// KindInvalidNumber describes an error that happens when a value is an invalid number.
+	KindInvalidNumber
+	// KindInvalidBool describes an error that happens when a value is an invalid boolean.
+	KindInvalidBool
 )
 
 // ParseError describes errors which happens while parse operation's parameters.
 type ParseError struct {
-	Value  interface{}
+	Kind   ParseErrorKind
 	Path   []interface{}
+	Value  interface{}
 	Reason string
 	Cause  error
 }
@@ -26,9 +42,10 @@ type ParseError struct {
 func (e *ParseError) Error() string {
 	var msg []string
 	if e.Path != nil {
-		msg = append(msg, fmt.Sprintf("failed to parse value %v at path %v", e.Value, e.Path))
-	} else {
-		msg = append(msg, fmt.Sprintf("failed to parse value %v", e.Value))
+		msg = append(msg, fmt.Sprintf("path %v", e.Path))
+	}
+	if e.Value != nil {
+		msg = append(msg, fmt.Sprintf("value %v", e.Value))
 	}
 	if e.Reason != "" {
 		msg = append(msg, e.Reason)
@@ -40,6 +57,7 @@ func (e *ParseError) Error() string {
 }
 
 // decodeParameter returns a value of an operation's parameter from HTTP request.
+// The function returns ParseError when HTTP request contains an invalid value of a parameter.
 func decodeParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, error) {
 	var decoder interface {
 		DecodePrimitive(param *openapi3.Parameter) (interface{}, error)
@@ -103,13 +121,9 @@ func (d *pathParamDecoder) DecodePrimitive(param *openapi3.Parameter) (interface
 	}
 	src, err := cutPrefix(raw, prefix)
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	val, err := parsePrimitive(src, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
-	return val, nil
+	return parsePrimitive(src, param.Schema)
 }
 
 func (d *pathParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface{}, error) {
@@ -148,13 +162,9 @@ func (d *pathParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface{}
 	}
 	src, err := cutPrefix(raw, prefix)
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	val, err := parseArray(strings.Split(src, delim), param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
-	return val, nil
+	return parseArray(strings.Split(src, delim), param.Schema)
 }
 
 func (d *pathParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string]interface{}, error) {
@@ -201,17 +211,13 @@ func (d *pathParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string]i
 	}
 	src, err := cutPrefix(raw, prefix)
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
 	props, err := propsFromString(src, propsDelim, valueDelim)
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	val, err := makeObject(props, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
-	return val, nil
+	return makeObject(props, param.Schema)
 }
 
 // paramKey returns a key to get a raw value of a path parameter.
@@ -233,7 +239,11 @@ func cutPrefix(raw, prefix string) (string, error) {
 		return raw, nil
 	}
 	if len(raw) < len(prefix) || raw[:len(prefix)] != prefix {
-		return "", &ParseError{Value: raw, Reason: fmt.Sprintf("a value must be prefixed with %q", prefix)}
+		return "", &ParseError{
+			Kind:   KindInvalidFormat,
+			Value:  raw,
+			Reason: fmt.Sprintf("a value must be prefixed with %q", prefix),
+		}
 	}
 	return raw[len(prefix):], nil
 }
@@ -257,11 +267,7 @@ func (d *queryParamDecoder) DecodePrimitive(param *openapi3.Parameter) (interfac
 		// HTTP request does not contain a value of the target query parameter.
 		return nil, nil
 	}
-	val, err := parsePrimitive(values[0], param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return parsePrimitive(values[0], param.Schema)
 }
 
 func (d *queryParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface{}, error) {
@@ -290,11 +296,7 @@ func (d *queryParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface{
 		}
 		values = strings.Split(values[0], delim)
 	}
-	val, err := parseArray(values, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return parseArray(values, param.Schema)
 }
 
 func (d *queryParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string]interface{}, error) {
@@ -347,16 +349,12 @@ func (d *queryParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string]
 
 	props, err := propsFn(d.input.GetQueryParams())
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
 	if props == nil {
 		return nil, nil
 	}
-	val, err := makeObject(props, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return makeObject(props, param.Schema)
 }
 
 // headerParamDecoder decodes values of header parameters.
@@ -374,11 +372,7 @@ func (d *headerParamDecoder) DecodePrimitive(param *openapi3.Parameter) (interfa
 	}
 
 	raw := d.input.Request.Header.Get(http.CanonicalHeaderKey(param.Name))
-	val, err := parsePrimitive(raw, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return parsePrimitive(raw, param.Schema)
 }
 
 func (d *headerParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface{}, error) {
@@ -391,16 +385,11 @@ func (d *headerParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface
 	}
 
 	raw := d.input.Request.Header.Get(http.CanonicalHeaderKey(param.Name))
-
-	val, err := parseArray(strings.Split(raw, ","), param.Schema)
 	if raw == "" {
 		// HTTP request does not contains a corresponding header
 		return nil, nil
 	}
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return parseArray(strings.Split(raw, ","), param.Schema)
 }
 
 func (d *headerParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string]interface{}, error) {
@@ -423,13 +412,9 @@ func (d *headerParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string
 	}
 	props, err := propsFromString(raw, ",", valueDelim)
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	val, err := makeObject(props, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
-	return val, nil
+	return makeObject(props, param.Schema)
 }
 
 // cookieParamDecoder decodes values of cookie parameters.
@@ -454,11 +439,7 @@ func (d *cookieParamDecoder) DecodePrimitive(param *openapi3.Parameter) (interfa
 	if err != nil {
 		return nil, fmt.Errorf("decode param %q: %s", param.Name, err)
 	}
-	val, err := parsePrimitive(cookie.Value, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return parsePrimitive(cookie.Value, param.Schema)
 }
 
 func (d *cookieParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface{}, error) {
@@ -478,11 +459,7 @@ func (d *cookieParamDecoder) DecodeArray(param *openapi3.Parameter) ([]interface
 	if err != nil {
 		return nil, fmt.Errorf("decode param %q: %s", param.Name, err)
 	}
-	val, err := parseArray(strings.Split(cookie.Value, ","), param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	return val, nil
+	return parseArray(strings.Split(cookie.Value, ","), param.Schema)
 }
 
 func (d *cookieParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string]interface{}, error) {
@@ -504,13 +481,9 @@ func (d *cookieParamDecoder) DecodeObject(param *openapi3.Parameter) (map[string
 	}
 	props, err := propsFromString(cookie.Value, ",", ",")
 	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
-	}
-	val, err := makeObject(props, param.Schema)
-	if err != nil {
-		return nil, &RequestError{Input: d.input, Parameter: param, Reason: errMsgInvalidValue, Err: err}
+		return nil, err
 	}
-	return val, nil
+	return makeObject(props, param.Schema)
 }
 
 // propsFromString returns a properties map that is created by splitting a source string by propDelim and valueDelim.
@@ -527,6 +500,7 @@ func propsFromString(src, propDelim, valueDelim string) (map[string]string, erro
 		// to an array with an even number of items.
 		if len(pairs)%2 != 0 {
 			return nil, &ParseError{
+				Kind:   KindInvalidFormat,
 				Value:  src,
 				Reason: fmt.Sprintf("a value must be a list of object's properties in format \"name%svalue\" separated by %s", valueDelim, propDelim),
 			}
@@ -543,6 +517,7 @@ func propsFromString(src, propDelim, valueDelim string) (map[string]string, erro
 		prop := strings.Split(pair, valueDelim)
 		if len(prop) != 2 {
 			return nil, &ParseError{
+				Kind:   KindInvalidFormat,
 				Value:  src,
 				Reason: fmt.Sprintf("a value must be a list of object's properties in format \"name%svalue\" separated by %s", valueDelim, propDelim),
 			}
@@ -561,12 +536,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string
 		value, err := parsePrimitive(props[propName], propSchema)
 		if err != nil {
 			if v, ok := err.(*ParseError); ok {
-				return nil, &ParseError{
-					Value:  v.Value,
-					Reason: v.Reason,
-					Cause:  v.Cause,
-					Path:   []interface{}{propName},
-				}
+				return nil, &ParseError{Path: []interface{}{propName}, Cause: v}
 			}
 			return nil, err
 		}
@@ -584,12 +554,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err
 		item, err := parsePrimitive(v, schemaRef.Value.Items)
 		if err != nil {
 			if v, ok := err.(*ParseError); ok {
-				return nil, &ParseError{
-					Value:  v.Value,
-					Reason: v.Reason,
-					Cause:  v.Cause,
-					Path:   []interface{}{i},
-				}
+				return nil, &ParseError{Path: []interface{}{i}, Cause: v}
 			}
 			return nil, err
 		}
@@ -606,16 +571,22 @@ func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error)
 		return nil, nil
 	}
 	switch schema.Value.Type {
-	case "integer", "number":
+	case "integer":
+		v, err := strconv.ParseFloat(raw, 64)
+		if err != nil {
+			return nil, &ParseError{Kind: KindInvalidInt, Value: raw, Reason: "an invalid interger", Cause: err}
+		}
+		return v, nil
+	case "number":
 		v, err := strconv.ParseFloat(raw, 64)
 		if err != nil {
-			return nil, &ParseError{Value: raw, Cause: err}
+			return nil, &ParseError{Kind: KindInvalidNumber, Value: raw, Reason: "an invalid number", Cause: err}
 		}
 		return v, nil
 	case "boolean":
 		v, err := strconv.ParseBool(raw)
 		if err != nil {
-			return nil, &ParseError{Value: raw, Cause: err}
+			return nil, &ParseError{Kind: KindInvalidBool, Value: raw, Reason: "an invalid number", Cause: err}
 		}
 		return v, nil
 	case "string":
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index 723b297..70d932d 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -12,6 +12,9 @@ import (
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
+// ErrInvalidRequired is an error that happens when a required value of a parameter is not defined.
+var ErrInvalidRequired = fmt.Errorf("must have a value")
+
 func ValidateRequest(c context.Context, input *RequestValidationInput) error {
 	options := input.Options
 	if options == nil {
@@ -66,30 +69,30 @@ func ValidateRequest(c context.Context, input *RequestValidationInput) error {
 	return nil
 }
 
+// ValidateParameter validates a parameter's value by JSON schema.
+// The function returns RequestError with a ParseError cause when unable to parse a value.
+// The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined.
+// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.
 func ValidateParameter(c context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error {
 	value, err := decodeParameter(parameter, input)
 	if err != nil {
-		return err
+		return &RequestError{Input: input, Parameter: parameter, Err: err}
 	}
 
 	// Validate a parameter's value.
 	if value == nil {
 		if parameter.Required {
-			return &RequestError{
-				Input:     input,
-				Parameter: parameter,
-				Reason:    "must have a value",
-			}
+			return &RequestError{Input: input, Parameter: parameter, Reason: "must have a value", Err: ErrInvalidRequired}
 		}
 		return nil
 	}
-	if schemaRef := parameter.Schema; schemaRef != nil {
-		// Only check schema if no transformation is needed
-		if schema := schemaRef.Value; schema.Type == "string" {
-			if err = schema.VisitJSON(value); err != nil {
-				return &RequestError{Input: input, Parameter: parameter, Err: err}
-			}
-		}
+	schema := parameter.Schema.Value
+	if schema == nil {
+		// A parameter's schema is not defined so skip validation of a parameter's value.
+		return nil
+	}
+	if err = schema.VisitJSON(value); err != nil {
+		return &RequestError{Input: input, Parameter: parameter, Err: err}
 	}
 	return nil
 }


Test patch:
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index f8b610a..8980c34 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -248,12 +248,16 @@ var schemaExamples = []schemaExample{
 			"2017-12-31T11:59:59Z",
 			"2017-12-31T11:59:59-11:30",
 			"2017-12-31T11:59:59+11:30",
+			"2017-12-31T11:59:59.999+11:30",
+			"2017-12-31T11:59:59.999Z",
 		},
 		AllInvalid: []interface{}{
 			nil,
 			3.14,
 			"2017-12-31",
 			"2017-12-31T11:59:59\n",
+			"2017-12-31T11:59:59.+11:30",
+			"2017-12-31T11:59:59.Z",
 		},
 	},
 
diff --git a/openapi3filter/param_decoder_test.go b/openapi3filter/param_decoder_test.go
index 55d86ff..be985bc 100644
--- a/openapi3filter/param_decoder_test.go
+++ b/openapi3filter/param_decoder_test.go
@@ -79,10 +79,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "label invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo"},
 				},
 				{
 					name:  "label explode",
@@ -94,10 +91,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "label explode invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo"},
 				},
 				{
 					name:  "matrix",
@@ -109,10 +103,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "matrix invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo"},
 				},
 				{
 					name:  "matrix explode",
@@ -124,10 +115,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "matrix explode invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo"},
 				},
 				{
 					name:  "default",
@@ -151,10 +139,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "integer invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidInt, Value: "foo"},
 				},
 				{
 					name:  "number",
@@ -166,10 +151,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "number invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidNumber, Value: "foo"},
 				},
 				{
 					name:  "boolean",
@@ -181,10 +163,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "boolean invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema},
 					path:  "/foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidBool, Value: "foo"},
 				},
 			},
 		},
@@ -213,10 +192,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "label invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema},
 					path:  "/foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
 				},
 				{
 					name:  "label explode",
@@ -228,10 +204,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "label explode invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema},
 					path:  "/foo.bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo.bar"},
 				},
 				{
 					name:  "matrix",
@@ -243,10 +216,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "matrix invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema},
 					path:  "/foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
 				},
 				{
 					name:  "matrix explode",
@@ -258,10 +228,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "matrix explode invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema},
 					path:  "/foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
 				},
 				{
 					name:  "default",
@@ -273,28 +240,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "invalid integer items",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)},
 					path:  "/1,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidInt, Value: "foo"}},
 				},
 				{
 					name:  "invalid number items",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)},
 					path:  "/1.1,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "foo"}},
 				},
 				{
 					name:  "invalid boolean items",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)},
 					path:  "/true,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidBool, Value: "foo"}},
 				},
 			},
 		},
@@ -323,10 +281,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "label invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema},
 					path:  "/id,foo,name,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"},
 				},
 				{
 					name:  "label explode",
@@ -338,10 +293,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "label explode invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema},
 					path:  "/id=foo.name=bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "id=foo.name=bar"},
 				},
 				{
 					name:  "matrix",
@@ -353,10 +305,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "matrix invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema},
 					path:  "/id,foo,name,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"},
 				},
 				{
 					name:  "matrix explode",
@@ -368,10 +317,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "matrix explode invalid",
 					param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema},
 					path:  "/id=foo;name=bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidFormat, Value: "id=foo;name=bar"},
 				},
 				{
 					name:  "default",
@@ -383,28 +329,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "invalid integer prop",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)},
 					path:  "/foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidInt, Value: "bar"}},
 				},
 				{
 					name:  "invalid number prop",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)},
 					path:  "/foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "bar"}},
 				},
 				{
 					name:  "invalid boolean prop",
 					param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)},
 					path:  "/foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidBool, Value: "bar"}},
 				},
 			},
 		},
@@ -445,10 +382,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "integer invalid",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema},
 					query: "param=foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidInt, Value: "foo"},
 				},
 				{
 					name:  "number",
@@ -460,10 +394,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "number invalid",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema},
 					query: "param=foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidNumber, Value: "foo"},
 				},
 				{
 					name:  "boolean",
@@ -475,10 +406,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "boolean invalid",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema},
 					query: "param=foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Kind: KindInvalidBool, Value: "foo"},
 				},
 			},
 		},
@@ -531,28 +459,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "invalid integer items",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)},
 					query: "param=1&param=foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidInt, Value: "foo"}},
 				},
 				{
 					name:  "invalid number items",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)},
 					query: "param=1.1&param=foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "foo"}},
 				},
 				{
 					name:  "invalid boolean items",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)},
 					query: "param=true&param=foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidBool, Value: "foo"}},
 				},
 			},
 		},
@@ -587,28 +506,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:  "invalid integer prop",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)},
 					query: "foo=bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidInt, Value: "bar"}},
 				},
 				{
 					name:  "invalid number prop",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)},
 					query: "foo=bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "bar"}},
 				},
 				{
 					name:  "invalid boolean prop",
 					param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)},
 					query: "foo=bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:   &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidBool, Value: "bar"}},
 				},
 			},
 		},
@@ -649,10 +559,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "integer invalid",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema},
 					header: "X-Param:foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Kind: KindInvalidInt, Value: "foo"},
 				},
 				{
 					name:   "number",
@@ -664,10 +571,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "number invalid",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema},
 					header: "X-Param:foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Kind: KindInvalidNumber, Value: "foo"},
 				},
 				{
 					name:   "boolean",
@@ -679,10 +583,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "boolean invalid",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema},
 					header: "X-Param:foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Kind: KindInvalidBool, Value: "foo"},
 				},
 			},
 		},
@@ -711,28 +612,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "invalid integer items",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)},
 					header: "X-Param:1,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidInt, Value: "foo"}},
 				},
 				{
 					name:   "invalid number items",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)},
 					header: "X-Param:1.1,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "foo"}},
 				},
 				{
 					name:   "invalid boolean items",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)},
 					header: "X-Param:true,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidBool, Value: "foo"}},
 				},
 			},
 		},
@@ -761,28 +653,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "invalid integer prop",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)},
 					header: "X-Param:foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidInt, Value: "bar"}},
 				},
 				{
 					name:   "invalid number prop",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)},
 					header: "X-Param:foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "bar"}},
 				},
 				{
 					name:   "invalid boolean prop",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)},
 					header: "X-Param:foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidBool, Value: "bar"}},
 				},
 			},
 		},
@@ -823,10 +706,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "integer invalid",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema},
 					cookie: "X-Param:foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Kind: KindInvalidInt, Value: "foo"},
 				},
 				{
 					name:   "number",
@@ -838,10 +718,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "number invalid",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema},
 					cookie: "X-Param:foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Kind: KindInvalidNumber, Value: "foo"},
 				},
 				{
 					name:   "boolean",
@@ -853,10 +730,7 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "boolean invalid",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema},
 					cookie: "X-Param:foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Kind: KindInvalidBool, Value: "foo"},
 				},
 			},
 		},
@@ -873,28 +747,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "invalid integer items",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)},
 					cookie: "X-Param:1,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidInt, Value: "foo"}},
 				},
 				{
 					name:   "invalid number items",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)},
 					cookie: "X-Param:1.1,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "foo"}},
 				},
 				{
 					name:   "invalid boolean items",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)},
 					cookie: "X-Param:true,foo",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidBool, Value: "foo"}},
 				},
 			},
 		},
@@ -911,28 +776,19 @@ func TestDecodeParameter(t *testing.T) {
 					name:   "invalid integer prop",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)},
 					cookie: "X-Param:foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidInt, Value: "bar"}},
 				},
 				{
 					name:   "invalid number prop",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)},
 					cookie: "X-Param:foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidNumber, Value: "bar"}},
 				},
 				{
 					name:   "invalid boolean prop",
 					param:  &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)},
 					cookie: "X-Param:foo,bar",
-					err: &RequestError{
-						Parameter: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)},
-						Reason:    "an invalid value",
-					},
+					err:    &ParseError{Path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidBool, Value: "bar"}},
 				},
 			},
 		},
@@ -994,7 +850,7 @@ func TestDecodeParameter(t *testing.T) {
 
 					if tc.err != nil {
 						require.Error(t, err)
-						require.Truef(t, matchParamError(err, tc.err), "got error %v, want error %v", err, tc.err)
+						require.Truef(t, matchParseError(err, tc.err), "got error:\n%v\nwant error:\n%v", err, tc.err)
 						return
 					}
 
@@ -1006,24 +862,26 @@ func TestDecodeParameter(t *testing.T) {
 	}
 }
 
-func matchParamError(got, want error) bool {
-	wErr, ok := want.(*RequestError)
+func matchParseError(got, want error) bool {
+	wErr, ok := want.(*ParseError)
 	if !ok {
 		return false
 	}
-	gErr, ok := got.(*RequestError)
+	gErr, ok := got.(*ParseError)
 	if !ok {
 		return false
 	}
-
-	if !reflect.DeepEqual(wErr.Parameter, gErr.Parameter) {
+	if wErr.Kind != gErr.Kind {
 		return false
 	}
-	if wErr.Status != gErr.Status {
+	if !reflect.DeepEqual(wErr.Value, gErr.Value) {
 		return false
 	}
-	if wErr.Reason != gErr.Reason {
+	if !reflect.DeepEqual(wErr.Path, gErr.Path) {
 		return false
 	}
+	if wErr.Cause != nil {
+		return matchParseError(gErr.Cause, wErr.Cause)
+	}
 	return true
 }