Repository: getkin/kin-openapi
Pull request #835 (2023-09-04 13:20:04)

openapi3: add missing document parameter validations
#834 


Patch:
diff --git a/go.mod b/go.mod
index 1b8855e..6bebb43 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/getkin/kin-openapi
 
-go 1.18
+go 1.20
 
 require (
 	github.com/go-openapi/jsonpointer v0.19.6
diff --git a/openapi3/header.go b/openapi3/header.go
index f359581..aea3bf8 100644
--- a/openapi3/header.go
+++ b/openapi3/header.go
@@ -89,7 +89,7 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er
 		return fmt.Errorf("header schema is invalid: %w", e)
 	}
 
-	if (header.Schema == nil) == (header.Content == nil) {
+	if (header.Schema == nil) == (len(header.Content) == 0) {
 		e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", header)
 		return fmt.Errorf("header schema is invalid: %w", e)
 	}
@@ -100,6 +100,11 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er
 	}
 
 	if content := header.Content; content != nil {
+		e := errors.New("parameter content must only contain one entry")
+		if len(content) > 1 {
+			return fmt.Errorf("header content is invalid: %w", e)
+		}
+
 		if err := content.Validate(ctx); err != nil {
 			return fmt.Errorf("header content is invalid: %w", err)
 		}
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index 07813a0..3ce2195 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -370,12 +370,17 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti
 		return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e)
 	}
 
-	if (parameter.Schema == nil) == (parameter.Content == nil) {
+	if (parameter.Schema == nil) == (len(parameter.Content) == 0) {
 		e := errors.New("parameter must contain exactly one of content and schema")
 		return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e)
 	}
 
 	if content := parameter.Content; content != nil {
+		e := errors.New("parameter content must only contain one entry")
+		if len(content) > 1 {
+			return fmt.Errorf("parameter %q content is invalid: %w", parameter.Name, e)
+		}
+
 		if err := content.Validate(ctx); err != nil {
 			return fmt.Errorf("parameter %q content is invalid: %w", parameter.Name, err)
 		}
diff --git a/openapi3/path_item.go b/openapi3/path_item.go
index fab75d9..2319997 100644
--- a/openapi3/path_item.go
+++ b/openapi3/path_item.go
@@ -207,5 +207,11 @@ func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption
 		}
 	}
 
+	if v := pathItem.Parameters; v != nil {
+		if err := v.Validate(ctx); err != nil {
+			return err
+		}
+	}
+
 	return validateExtensions(ctx, pathItem.Extensions)
 }


Test patch:
diff --git a/openapi3/parameter_issue834_test.go b/openapi3/parameter_issue834_test.go
new file mode 100644
index 0000000..1a15321
--- /dev/null
+++ b/openapi3/parameter_issue834_test.go
@@ -0,0 +1,114 @@
+package openapi3
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestPathItemParametersAreValidated(t *testing.T) {
+	spec := []byte(`
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: http://petstore.swagger.io/v1
+paths:
+  /pets:
+    parameters:
+      - in: invalid
+        name: test
+        schema:
+          type: string
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+      responses:
+        '200':
+          description: A paged array of pets
+`[1:])
+
+	loader := NewLoader()
+	doc, err := loader.LoadFromData(spec)
+	require.NoError(t, err)
+	err = doc.Validate(context.Background())
+	require.EqualError(t, err, `invalid paths: invalid path /pets: parameter can't have 'in' value "invalid"`)
+}
+
+func TestParameterMultipleContentEntries(t *testing.T) {
+	spec := []byte(`
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: http://petstore.swagger.io/v1
+paths:
+  /pets:
+    parameters:
+      - in: query
+        name: test
+        content:
+          application/json:
+            schema:
+              type: string
+          application/xml:
+            schema:
+              type: string
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+      responses:
+        '200':
+          description: A paged array of pets
+`[1:])
+
+	loader := NewLoader()
+	doc, err := loader.LoadFromData(spec)
+	require.NoError(t, err)
+	err = doc.Validate(context.Background())
+	require.EqualError(t, err, `invalid paths: invalid path /pets: parameter "test" content is invalid: parameter content must only contain one entry`)
+}
+
+func TestParameterEmptyContent(t *testing.T) {
+	spec := []byte(`
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: http://petstore.swagger.io/v1
+paths:
+  /pets:
+    parameters:
+      - in: query
+        name: test
+        content: {}
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+      responses:
+        '200':
+          description: A paged array of pets
+`[1:])
+
+	loader := NewLoader()
+	doc, err := loader.LoadFromData(spec)
+	require.NoError(t, err)
+	err = doc.Validate(context.Background())
+	require.EqualError(t, err, `invalid paths: invalid path /pets: parameter "test" schema is invalid: parameter must contain exactly one of content and schema`)
+}
diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go
index 5fb9be2..bebe056 100644
--- a/routers/gorillamux/router_test.go
+++ b/routers/gorillamux/router_test.go
@@ -50,21 +50,21 @@ 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")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("x").WithSchema(openapi3.NewStringSchema())},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("y").WithSchema(openapi3.NewFloat64Schema())},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("z").WithSchema(openapi3.NewIntegerSchema())},
 				},
 			},
 			"/books/{bookid}": &openapi3.PathItem{
 				Get: paramsGET,
 				Parameters: openapi3.Parameters{
-					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid").WithSchema(openapi3.NewStringSchema())},
 				},
 			},
 			"/books/{bookid}.json": &openapi3.PathItem{
 				Post: booksPOST,
 				Parameters: openapi3.Parameters{
-					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2").WithSchema(openapi3.NewStringSchema())},
 				},
 			},
 			"/partial": &openapi3.PathItem{
diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go
index e9b8759..554ecd1 100644
--- a/routers/legacy/router_test.go
+++ b/routers/legacy/router_test.go
@@ -49,21 +49,21 @@ 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")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("x").WithSchema(openapi3.NewStringSchema())},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("y").WithSchema(openapi3.NewFloat64Schema())},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("z").WithSchema(openapi3.NewIntegerSchema())},
 				},
 			},
 			"/books/{bookid}": &openapi3.PathItem{
 				Get: paramsGET,
 				Parameters: openapi3.Parameters{
-					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid").WithSchema(openapi3.NewStringSchema())},
 				},
 			},
 			"/books/{bookid2}.json": &openapi3.PathItem{
 				Post: booksPOST,
 				Parameters: openapi3.Parameters{
-					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")},
+					&openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2").WithSchema(openapi3.NewStringSchema())},
 				},
 			},
 			"/partial": &openapi3.PathItem{