Repository: getkin/kin-openapi
Pull request #227 (2020-06-15 09:30:36)

validate path params against path
Fixes https://github.com/getkin/kin-openapi/issues/223


Patch:
diff --git a/openapi3/info.go b/openapi3/info.go
index 59e03cc..25e675e 100644
--- a/openapi3/info.go
+++ b/openapi3/info.go
@@ -3,7 +3,6 @@ package openapi3
 import (
 	"context"
 	"errors"
-	"fmt"
 
 	"github.com/getkin/kin-openapi/jsoninfo"
 )
@@ -30,22 +29,22 @@ func (value *Info) UnmarshalJSON(data []byte) error {
 func (value *Info) Validate(c context.Context) error {
 	if contact := value.Contact; contact != nil {
 		if err := contact.Validate(c); err != nil {
-			return fmt.Errorf("Error when validating Contact: %s", err.Error())
+			return err
 		}
 	}
 
 	if license := value.License; license != nil {
 		if err := license.Validate(c); err != nil {
-			return fmt.Errorf("Error when validating License: %s", err.Error())
+			return err
 		}
 	}
 
 	if value.Version == "" {
-		return errors.New("Variable 'version' must be a non-empty JSON string")
+		return errors.New("value of version must be a non-empty JSON string")
 	}
 
 	if value.Title == "" {
-		return errors.New("Variable 'title' must be a non-empty JSON string")
+		return errors.New("value of title must be a non-empty JSON string")
 	}
 
 	return nil
@@ -88,7 +87,7 @@ func (value *License) UnmarshalJSON(data []byte) error {
 
 func (value *License) Validate(c context.Context) error {
 	if value.Name == "" {
-		return errors.New("Variable 'name' must be a non-empty JSON string")
+		return errors.New("value of license name must be a non-empty JSON string")
 	}
 	return nil
 }
diff --git a/openapi3/operation.go b/openapi3/operation.go
index 5e367e0..9e64f03 100644
--- a/openapi3/operation.go
+++ b/openapi3/operation.go
@@ -96,7 +96,7 @@ func (operation *Operation) Validate(c context.Context) error {
 			return err
 		}
 	} else {
-		return errors.New("Variable 'Responses' must be a JSON object")
+		return errors.New("value of responses must be a JSON object")
 	}
 	return nil
 }
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index 30ec868..1e2f55e 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -27,22 +27,18 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter {
 }
 
 func (parameters Parameters) Validate(c context.Context) error {
-	m := make(map[string]struct{})
+	dupes := make(map[string]struct{})
 	for _, item := range parameters {
-		if err := item.Validate(c); err != nil {
-			return err
-		}
 		if v := item.Value; v != nil {
-			in := v.In
-			name := v.Name
-			key := in + ":" + name
-			if _, exists := m[key]; exists {
-				return fmt.Errorf("More than one '%s' parameter has name '%s'", in, name)
-			}
-			m[key] = struct{}{}
-			if err := item.Validate(c); err != nil {
-				return err
+			key := v.In + ":" + v.Name
+			if _, ok := dupes[key]; ok {
+				return fmt.Errorf("more than one %q parameter has name %q", v.In, v.Name)
 			}
+			dupes[key] = struct{}{}
+		}
+
+		if err := item.Validate(c); err != nil {
+			return err
 		}
 	}
 	return nil
@@ -163,7 +159,7 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error)
 
 func (parameter *Parameter) Validate(c context.Context) error {
 	if parameter.Name == "" {
-		return errors.New("Parameter name can't be blank")
+		return errors.New("parameter name can't be blank")
 	}
 	in := parameter.In
 	switch in {
@@ -173,7 +169,7 @@ func (parameter *Parameter) Validate(c context.Context) error {
 		ParameterInHeader,
 		ParameterInCookie:
 	default:
-		return fmt.Errorf("Parameter can't have 'in' value '%s'", parameter.In)
+		return fmt.Errorf("parameter can't have 'in' value %q", parameter.In)
 	}
 
 	// Validate a parameter's serialization method.
@@ -206,26 +202,22 @@ func (parameter *Parameter) Validate(c context.Context) error {
 		smSupported = true
 	}
 	if !smSupported {
-		e := fmt.Errorf("Serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in)
-		return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, e)
+		e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in)
+		return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e)
 	}
 
-	if parameter.Schema != nil && parameter.Content != nil {
-		return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name,
-			errors.New("Cannot contain both schema and content in a parameter"))
-	}
-	if parameter.Schema == nil && parameter.Content == nil {
-		return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name,
-			errors.New("A parameter MUST contain either a schema property, or a content property"))
+	if (parameter.Schema == nil) == (parameter.Content == nil) {
+		e := errors.New("parameter must contain exactly one of content and schema")
+		return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e)
 	}
 	if schema := parameter.Schema; schema != nil {
 		if err := schema.Validate(c); err != nil {
-			return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, err)
+			return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err)
 		}
 	}
 	if content := parameter.Content; content != nil {
 		if err := content.Validate(c); err != nil {
-			return fmt.Errorf("Parameter content is invalid: %v", err)
+			return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err)
 		}
 	}
 	return nil
diff --git a/openapi3/paths.go b/openapi3/paths.go
index d738e9d..0bfeb2c 100644
--- a/openapi3/paths.go
+++ b/openapi3/paths.go
@@ -12,14 +12,38 @@ type Paths map[string]*PathItem
 func (paths Paths) Validate(c context.Context) error {
 	normalizedPaths := make(map[string]string)
 	for path, pathItem := range paths {
-		normalizedPath := normalizePathKey(path)
-		if oldPath, exists := normalizedPaths[normalizedPath]; exists {
-			return fmt.Errorf("Conflicting paths '%v' and '%v'", path, oldPath)
-		}
 		if path == "" || path[0] != '/' {
-			return fmt.Errorf("Path '%v' does not start with '/'", path)
+			return fmt.Errorf("path %q does not start with a forward slash (/)", path)
+		}
+
+		normalizedPath, pathParamsCount := normalizeTemplatedPath(path)
+		if oldPath, ok := normalizedPaths[normalizedPath]; ok {
+			return fmt.Errorf("conflicting paths %q and %q", path, oldPath)
 		}
 		normalizedPaths[path] = path
+
+		var globalCount uint
+		for _, parameterRef := range pathItem.Parameters {
+			if parameterRef != nil {
+				if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
+					globalCount++
+				}
+			}
+		}
+		for method, operation := range pathItem.Operations() {
+			var count uint
+			for _, parameterRef := range operation.Parameters {
+				if parameterRef != nil {
+					if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
+						count++
+					}
+				}
+			}
+			if count+globalCount != pathParamsCount {
+				return fmt.Errorf("operation %s %s must define exactly all path parameters", method, path)
+			}
+		}
+
 		if err := pathItem.Validate(c); err != nil {
 			return err
 		}
@@ -46,37 +70,37 @@ func (paths Paths) Find(key string) *PathItem {
 		return pathItem
 	}
 
-	// Use normalized keys
-	normalizedSearchedPath := normalizePathKey(key)
+	normalizedPath, expected := normalizeTemplatedPath(key)
 	for path, pathItem := range paths {
-		normalizedPath := normalizePathKey(path)
-		if normalizedPath == normalizedSearchedPath {
+		pathNormalized, got := normalizeTemplatedPath(path)
+		if got == expected && pathNormalized == normalizedPath {
 			return pathItem
 		}
 	}
 	return nil
 }
 
-func normalizePathKey(key string) string {
-	// If the argument has no path variables, return the argument
-	if strings.IndexByte(key, '{') < 0 {
-		return key
+func normalizeTemplatedPath(path string) (string, uint) {
+	if strings.IndexByte(path, '{') < 0 {
+		return path, 0
 	}
 
-	// Allocate buffer
-	buf := make([]byte, 0, len(key))
+	var buf strings.Builder
+	buf.Grow(len(path))
 
-	// Visit each byte
-	isVariable := false
-	for i := 0; i < len(key); i++ {
-		c := key[i]
+	var (
+		cc         rune
+		count      uint
+		isVariable bool
+	)
+	for i, c := range path {
 		if isVariable {
 			if c == '}' {
 				// End path variables
 				// First append possible '*' before this character
 				// The character '}' will be appended
-				if i > 0 && key[i-1] == '*' {
-					buf = append(buf, '*')
+				if i > 0 && cc == '*' {
+					buf.WriteRune(cc)
 				}
 				isVariable = false
 			} else {
@@ -87,10 +111,12 @@ func normalizePathKey(key string) string {
 			// Begin path variable
 			// The character '{' will be appended
 			isVariable = true
+			count++
 		}
 
 		// Append the character
-		buf = append(buf, c)
+		buf.WriteRune(c)
+		cc = c
 	}
-	return string(buf)
+	return buf.String(), count
 }
diff --git a/openapi3/response.go b/openapi3/response.go
index 3e1a732..a89b28e 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -27,7 +27,7 @@ func (responses Responses) Get(status int) *ResponseRef {
 
 func (responses Responses) Validate(c context.Context) error {
 	if len(responses) == 0 {
-		return errors.New("The Responses Object MUST contain at least one response code")
+		return errors.New("the responses object MUST contain at least one response code")
 	}
 	for _, v := range responses {
 		if err := v.Validate(c); err != nil {
@@ -80,7 +80,7 @@ func (response *Response) UnmarshalJSON(data []byte) error {
 
 func (response *Response) Validate(c context.Context) error {
 	if response.Description == nil {
-		return errors.New("A short description of the response is required")
+		return errors.New("a short description of the response is required")
 	}
 
 	if content := response.Content; content != nil {
diff --git a/openapi3/server.go b/openapi3/server.go
index 4392b09..2594d2b 100644
--- a/openapi3/server.go
+++ b/openapi3/server.go
@@ -114,7 +114,7 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) {
 
 func (server *Server) Validate(c context.Context) (err error) {
 	if server.URL == "" {
-		return errors.New("Variable 'URL' must be a non-empty JSON string")
+		return errors.New("value of url must be a non-empty JSON string")
 	}
 	for _, v := range server.Variables {
 		if err = v.Validate(c); err != nil {
@@ -135,7 +135,7 @@ func (serverVariable *ServerVariable) Validate(c context.Context) error {
 	switch serverVariable.Default.(type) {
 	case float64, string:
 	default:
-		return errors.New("Variable 'default' must be either JSON number or JSON string")
+		return errors.New("value of default must be either JSON number or JSON string")
 	}
 	for _, item := range serverVariable.Enum {
 		switch item.(type) {
diff --git a/openapi3/swagger.go b/openapi3/swagger.go
index 6ca8f1e..06be8a3 100644
--- a/openapi3/swagger.go
+++ b/openapi3/swagger.go
@@ -11,12 +11,12 @@ import (
 type Swagger struct {
 	ExtensionProps
 	OpenAPI      string               `json:"openapi" yaml:"openapi"` // Required
-	Info         *Info                `json:"info" yaml:"info"`       // Required
-	Servers      Servers              `json:"servers,omitempty" yaml:"servers,omitempty"`
-	Paths        Paths                `json:"paths" yaml:"paths"` // Required
 	Components   Components           `json:"components,omitempty" yaml:"components,omitempty"`
-	Tags         Tags                 `json:"tags,omitempty" yaml:"tags,omitempty"`
+	Info         *Info                `json:"info" yaml:"info"`   // Required
+	Paths        Paths                `json:"paths" yaml:"paths"` // Required
 	Security     SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"`
+	Servers      Servers              `json:"servers,omitempty" yaml:"servers,omitempty"`
+	Tags         Tags                 `json:"tags,omitempty" yaml:"tags,omitempty"`
 	ExternalDocs *ExternalDocs        `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
 }
 
@@ -48,34 +48,57 @@ func (swagger *Swagger) AddServer(server *Server) {
 
 func (swagger *Swagger) Validate(c context.Context) error {
 	if swagger.OpenAPI == "" {
-		return errors.New("Variable 'openapi' must be a non-empty JSON string")
+		return errors.New("value of openapi must be a non-empty JSON string")
 	}
-	if err := swagger.Components.Validate(c); err != nil {
-		return fmt.Errorf("Error when validating Components: %s", err.Error())
+
+	// NOTE: only mention info/components/paths/... key in this func's errors.
+
+	{
+		wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) }
+		if err := swagger.Components.Validate(c); err != nil {
+			return wrap(err)
+		}
 	}
-	if v := swagger.Security; v != nil {
-		if err := v.Validate(c); err != nil {
-			return fmt.Errorf("Error when validating Security: %s", err.Error())
+
+	{
+		wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) }
+		if v := swagger.Info; v != nil {
+			if err := v.Validate(c); err != nil {
+				return wrap(err)
+			}
+		} else {
+			return wrap(errors.New("must be a JSON object"))
 		}
 	}
-	if v := swagger.Servers; v != nil {
-		if err := v.Validate(c); err != nil {
-			return fmt.Errorf("Error when validating Servers: %s", err.Error())
+
+	{
+		wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) }
+		if v := swagger.Paths; v != nil {
+			if err := v.Validate(c); err != nil {
+				return wrap(err)
+			}
+		} else {
+			return wrap(errors.New("must be a JSON object"))
 		}
 	}
-	if v := swagger.Paths; v != nil {
-		if err := v.Validate(c); err != nil {
-			return fmt.Errorf("Error when validating Paths: %s", err.Error())
+
+	{
+		wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) }
+		if v := swagger.Security; v != nil {
+			if err := v.Validate(c); err != nil {
+				return wrap(err)
+			}
 		}
-	} else {
-		return errors.New("Variable 'paths' must be a JSON object")
 	}
-	if v := swagger.Info; v != nil {
-		if err := v.Validate(c); err != nil {
-			return fmt.Errorf("Error when validating Info: %s", err.Error())
+
+	{
+		wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) }
+		if v := swagger.Servers; v != nil {
+			if err := v.Validate(c); err != nil {
+				return wrap(err)
+			}
 		}
-	} else {
-		return errors.New("Variable 'info' must be a JSON object")
 	}
+
 	return nil
 }


Test patch:
diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go
index 22f325e..fe59c60 100644
--- a/openapi3/operation_test.go
+++ b/openapi3/operation_test.go
@@ -53,7 +53,7 @@ func TestOperationValidation(t *testing.T) {
 		{
 			"when no Responses object is provided",
 			operationWithoutResponses(),
-			errors.New("Variable 'Responses' must be a JSON object"),
+			errors.New("value of responses must be a JSON object"),
 		},
 		{
 			"when a Responses object is provided",
diff --git a/openapi3/parameter_issue223_test.go b/openapi3/parameter_issue223_test.go
new file mode 100644
index 0000000..ae1ddd2
--- /dev/null
+++ b/openapi3/parameter_issue223_test.go
@@ -0,0 +1,116 @@
+package openapi3
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestPathParametersMatchPath(t *testing.T) {
+	spec := `
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: http://petstore.swagger.io/v1
+paths:
+  /pets:
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+
+      responses:
+        '200':
+          description: A paged array of pets
+          headers:
+            x-next:
+              description: A link to the next page of responses
+              schema:
+                type: string
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Pets"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+    post:
+      summary: Create a pet
+      operationId: createPets
+      tags:
+        - pets
+      responses:
+        '201':
+          description: Null response
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+  /pets/{petId}:
+    get:
+      summary: Info for a specific pet
+      operationId: showPetById
+      tags:
+        - pets
+       # <------------------ no parameters
+      responses:
+        '200':
+          description: Expected response to a valid request
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Pet"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+components:
+  schemas:
+    Pet:
+      type: object
+      required:
+        - id
+        - name
+      properties:
+        id:
+          type: integer
+          format: int64
+        name:
+          type: string
+        tag:
+          type: string
+    Pets:
+      type: array
+      items:
+        $ref: "#/components/schemas/Pet"
+    Error:
+      type: object
+      required:
+        - code
+        - message
+      properties:
+        code:
+          type: integer
+          format: int32
+        message:
+          type: string
+`
+
+	doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec))
+	require.NoError(t, err)
+	err = doc.Validate(context.Background())
+	require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters`)
+}
diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go
index 6dae39d..2a17580 100644
--- a/openapi3/response_issue224_test.go
+++ b/openapi3/response_issue224_test.go
@@ -456,5 +456,5 @@ func TestEmptyResponsesAreInvalid(t *testing.T) {
 	doc, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec))
 	require.NoError(t, err)
 	err = doc.Validate(context.Background())
-	require.Error(t, err)
+	require.EqualError(t, err, `invalid paths: the responses object MUST contain at least one response code`)
 }
diff --git a/openapi3/server_test.go b/openapi3/server_test.go
index b877a35..beafcaa 100644
--- a/openapi3/server_test.go
+++ b/openapi3/server_test.go
@@ -70,7 +70,7 @@ func TestServerValidation(t *testing.T) {
 		{
 			"when no URL is provided",
 			invalidServer(),
-			errors.New("Variable 'URL' must be a non-empty JSON string"),
+			errors.New("value of url must be a non-empty JSON string"),
 		},
 		{
 			"when a URL is provided",
diff --git a/openapi3/swagger_loader_paths_test.go b/openapi3/swagger_loader_paths_test.go
index e805d3a..9605b07 100644
--- a/openapi3/swagger_loader_paths_test.go
+++ b/openapi3/swagger_loader_paths_test.go
@@ -24,7 +24,7 @@ paths:
 `
 
 	for path, expectedErr := range map[string]string{
-		"foo/bar":  "Error when validating Paths: Path 'foo/bar' does not start with '/'",
+		"foo/bar":  "invalid paths: path \"foo/bar\" does not start with a forward slash (/)",
 		"/foo/bar": "",
 	} {
 		loader := openapi3.NewSwaggerLoader()
diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go
index 920911a..9603832 100644
--- a/openapi3/swagger_loader_test.go
+++ b/openapi3/swagger_loader_test.go
@@ -97,7 +97,7 @@ func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) {
 	doc, err := loader.LoadSwaggerFromData(source)
 	require.NoError(t, err)
 	err = doc.Validate(loader.Context)
-	require.EqualError(t, err, "Error when validating Paths: Found unresolved ref: ''")
+	require.EqualError(t, err, "invalid paths: Found unresolved ref: ''")
 }
 
 func TestResolveResponseExampleRef(t *testing.T) {
diff --git a/openapi3/swagger_test.go b/openapi3/swagger_test.go
index d4c4339..06c478c 100644
--- a/openapi3/swagger_test.go
+++ b/openapi3/swagger_test.go
@@ -21,7 +21,7 @@ func TestRefsJSON(t *testing.T) {
 
 	t.Log("Unmarshal *openapi3.Swagger from JSON")
 	docA := &openapi3.Swagger{}
-	err = json.Unmarshal(specJSON, &docA)
+	err = json.Unmarshal([]byte(specJSON), &docA)
 	require.NoError(t, err)
 	require.NotEmpty(t, data)
 
@@ -44,7 +44,7 @@ func TestRefsJSON(t *testing.T) {
 	require.NoError(t, err)
 	dataB, err := json.Marshal(docB)
 	require.NoError(t, err)
-	require.JSONEq(t, string(data), string(specJSON))
+	require.JSONEq(t, string(data), specJSON)
 	require.JSONEq(t, string(data), string(dataA))
 	require.JSONEq(t, string(data), string(dataB))
 }
@@ -59,7 +59,7 @@ func TestRefsYAML(t *testing.T) {
 
 	t.Log("Unmarshal *openapi3.Swagger from YAML")
 	docA := &openapi3.Swagger{}
-	err = yaml.Unmarshal(specYAML, &docA)
+	err = yaml.Unmarshal([]byte(specYAML), &docA)
 	require.NoError(t, err)
 	require.NotEmpty(t, data)
 
@@ -82,7 +82,7 @@ func TestRefsYAML(t *testing.T) {
 	require.NoError(t, err)
 	dataB, err := yaml.Marshal(docB)
 	require.NoError(t, err)
-	eqYAML(t, data, specYAML)
+	eqYAML(t, data, []byte(specYAML))
 	eqYAML(t, data, dataA)
 	eqYAML(t, data, dataB)
 }
@@ -96,7 +96,7 @@ func eqYAML(t *testing.T, expected, actual []byte) {
 	require.Equal(t, e, a)
 }
 
-var specYAML = []byte(`
+var specYAML = `
 openapi: '3.0'
 info:
   title: MyAPI
@@ -148,9 +148,9 @@ components:
       name: token
     someSecurityScheme:
       "$ref": "#/components/securitySchemes/otherSecurityScheme"
-`)
+`
 
-var specJSON = []byte(`
+var specJSON = `
 {
   "openapi": "3.0",
   "info": {
@@ -236,7 +236,7 @@ var specJSON = []byte(`
     }
   }
 }
-`)
+`
 
 func spec() *openapi3.Swagger {
 	parameter := &openapi3.Parameter{
@@ -351,12 +351,12 @@ func spec() *openapi3.Swagger {
 func TestValidation(t *testing.T) {
 	tests := []struct {
 		name          string
-		input         []byte
+		input         string
 		expectedError error
 	}{
 		{
 			"when no OpenAPI property is supplied",
-			[]byte(`
+			`
 info:
   title: "Hello World REST APIs"
   version: "1.0"
@@ -380,12 +380,12 @@ paths:
       responses:
         200:
           description: "Get a single greeting object"
-`),
-			errors.New("Variable 'openapi' must be a non-empty JSON string"),
+`,
+			errors.New("value of openapi must be a non-empty JSON string"),
 		},
 		{
 			"when an empty OpenAPI property is supplied",
-			[]byte(`
+			`
 openapi: ''
 info:
   title: "Hello World REST APIs"
@@ -410,12 +410,12 @@ paths:
       responses:
         200:
           description: "Get a single greeting object"
-`),
-			errors.New("Variable 'openapi' must be a non-empty JSON string"),
+`,
+			errors.New("value of openapi must be a non-empty JSON string"),
 		},
 		{
 			"when the Info property is not supplied",
-			[]byte(`
+			`
 openapi: '1.0'
 paths:
   "/api/v2/greetings.json":
@@ -437,22 +437,22 @@ paths:
       responses:
         200:
           description: "Get a single greeting object"
-`),
-			errors.New("Variable 'info' must be a JSON object"),
+`,
+			errors.New("invalid info: must be a JSON object"),
 		},
 		{
 			"when the Paths property is not supplied",
-			[]byte(`
+			`
 openapi: '1.0'
 info:
   title: "Hello World REST APIs"
   version: "1.0"
-`),
-			errors.New("Variable 'paths' must be a JSON object"),
+`,
+			errors.New("invalid paths: must be a JSON object"),
 		},
 		{
 			"when a valid spec is supplied",
-			[]byte(`
+			`
 openapi: 3.0.2
 info:
   title: "Hello World REST APIs"
@@ -490,7 +490,7 @@ components:
           properties:
             description:
               type: string
-`),
+`,
 			nil,
 		},
 	}
@@ -498,7 +498,7 @@ components:
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			doc := &openapi3.Swagger{}
-			err := yaml.Unmarshal(test.input, &doc)
+			err := yaml.Unmarshal([]byte(test.input), &doc)
 			require.NoError(t, err)
 
 			c := context.Background()
diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go
index 2865cb8..5a72411 100644
--- a/openapi3filter/router_test.go
+++ b/openapi3filter/router_test.go
@@ -52,6 +52,11 @@ func TestRouter(t *testing.T) {
 			},
 			"/params/{x}/{y}/{z*}": &openapi3.PathItem{
 				Get: paramsGET,
+				Parameters: openapi3.Parameters{
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")},
+				},
 			},
 			"/partial": &openapi3.PathItem{
 				Get: partialGET,