Repository: getkin/kin-openapi
Pull request #885 (2023-12-08 08:30:34)

feat: support default value binding with allOfSchema
- fix https://github.com/getkin/kin-openapi/issues/884

📝 nested complex structure is not supported with this PR

podhmo: need tests?

Patch:
diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt
index 6481fc0..7c14783 100644
--- a/.github/docs/openapi3filter.txt
+++ b/.github/docs/openapi3filter.txt
@@ -201,6 +201,9 @@ type Options struct {
 	// Set ExcludeRequestBody so ValidateRequest skips request body validation
 	ExcludeRequestBody bool
 
+	// Set ExcludeRequestQueryParams so ValidateRequest skips request query params validation
+	ExcludeRequestQueryParams bool
+
 	// Set ExcludeResponseBody so ValidateResponse skips response body validation
 	ExcludeResponseBody bool
 
diff --git a/openapi3filter/options.go b/openapi3filter/options.go
index 4bda60a..9b915c5 100644
--- a/openapi3filter/options.go
+++ b/openapi3filter/options.go
@@ -7,6 +7,9 @@ type Options struct {
 	// Set ExcludeRequestBody so ValidateRequest skips request body validation
 	ExcludeRequestBody bool
 
+	// Set ExcludeRequestQueryParams so ValidateRequest skips request query params validation
+	ExcludeRequestQueryParams bool
+
 	// Set ExcludeResponseBody so ValidateResponse skips response body validation
 	ExcludeResponseBody bool
 
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index db3b075..7a8b5ca 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -75,6 +75,9 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er
 
 	// For each parameter of the Operation
 	for _, parameter := range operationParameters {
+		if options.ExcludeRequestQueryParams && parameter.Value.In == openapi3.ParameterInQuery {
+			continue
+		}
 		if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError {
 			return
 		}
@@ -136,24 +139,33 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param
 	}
 
 	// Set default value if needed
-	if !options.SkipSettingDefaults && value == nil && schema != nil && schema.Default != nil {
+	if !options.SkipSettingDefaults && value == nil && schema != nil {
 		value = schema.Default
-		req := input.Request
-		switch parameter.In {
-		case openapi3.ParameterInPath:
-			// Path parameters are required.
-			// Next check `parameter.Required && !found` will catch this.
-		case openapi3.ParameterInQuery:
-			q := req.URL.Query()
-			q.Add(parameter.Name, fmt.Sprintf("%v", value))
-			req.URL.RawQuery = q.Encode()
-		case openapi3.ParameterInHeader:
-			req.Header.Add(parameter.Name, fmt.Sprintf("%v", value))
-		case openapi3.ParameterInCookie:
-			req.AddCookie(&http.Cookie{
-				Name:  parameter.Name,
-				Value: fmt.Sprintf("%v", value),
-			})
+		for _, subSchema := range schema.AllOf {
+			if subSchema.Value.Default != nil {
+				value = subSchema.Value.Default
+				break // This is not a validation of the schema itself, so use the first default value.
+			}
+		}
+
+		if value != nil {
+			req := input.Request
+			switch parameter.In {
+			case openapi3.ParameterInPath:
+				// Path parameters are required.
+				// Next check `parameter.Required && !found` will catch this.
+			case openapi3.ParameterInQuery:
+				q := req.URL.Query()
+				q.Add(parameter.Name, fmt.Sprintf("%v", value))
+				req.URL.RawQuery = q.Encode()
+			case openapi3.ParameterInHeader:
+				req.Header.Add(parameter.Name, fmt.Sprintf("%v", value))
+			case openapi3.ParameterInCookie:
+				req.AddCookie(&http.Cookie{
+					Name:  parameter.Name,
+					Value: fmt.Sprintf("%v", value),
+				})
+			}
 		}
 	}
 


Test patch:
diff --git a/openapi3filter/issue884_test.go b/openapi3filter/issue884_test.go
new file mode 100644
index 0000000..ea6e461
--- /dev/null
+++ b/openapi3filter/issue884_test.go
@@ -0,0 +1,113 @@
+package openapi3filter
+
+import (
+	"net/http"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIssue884(t *testing.T) {
+	loader := openapi3.NewLoader()
+	ctx := loader.Context
+	spec := `
+  openapi: 3.0.0
+  info:
+    version: 1.0.0
+    title: Sample API
+  components:
+    schemas:
+      TaskSortEnum:
+        enum:
+          - createdAt
+          - -createdAt
+          - updatedAt
+          - -updatedAt
+  paths:
+    /tasks:
+      get:
+        operationId: ListTask
+        parameters:
+          - in: query
+            name: withDefault
+            schema:
+              allOf:
+                - $ref: '#/components/schemas/TaskSortEnum'
+                - default: -createdAt
+          - in: query
+            name: withoutDefault
+            schema:
+              allOf:
+                - $ref: '#/components/schemas/TaskSortEnum'
+          - in: query
+            name: withManyDefaults
+            schema:
+              allOf:
+                - default: -updatedAt
+                - $ref: '#/components/schemas/TaskSortEnum'
+                - default: -createdAt
+        responses:
+          '200':
+            description: Successful response
+  `[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
+		expectedQuery url.Values
+	}{
+		{
+			name: "no defaults are added to requests",
+			options: &Options{
+				SkipSettingDefaults: true,
+			},
+			expectedQuery: url.Values{},
+		},
+
+		{
+			name: "defaults are added to requests",
+			expectedQuery: url.Values{
+				"withDefault":      []string{"-createdAt"},
+				"withManyDefaults": []string{"-updatedAt"}, // first default is win
+			},
+		},
+	}
+
+	for _, testcase := range tests {
+		t.Run(testcase.name, func(t *testing.T) {
+			httpReq, err := http.NewRequest(http.MethodGet, "/tasks", nil)
+			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)
+
+			q := httpReq.URL.Query()
+			assert.Equal(t, testcase.expectedQuery, q)
+		})
+	}
+}
diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go
index 8da550c..1322a55 100644
--- a/openapi3filter/validate_request_test.go
+++ b/openapi3filter/validate_request_test.go
@@ -221,3 +221,49 @@ components:
 		})
 	}
 }
+
+func TestValidateRequestExcludeQueryParams(t *testing.T) {
+	const spec = `
+openapi: 3.0.0
+info:
+  title: 'Validator'
+  version: 0.0.1
+paths:
+  /category:
+    post:
+      parameters:
+        - name: category
+          in: query
+          schema:
+            type: integer
+          required: true
+      responses:
+        '200':
+          description: Ok
+`
+	req, err := http.NewRequest(http.MethodPost, "/category?category=foo", nil)
+	require.NoError(t, err)
+	router := setupTestRouter(t, spec)
+	route, pathParams, err := router.FindRoute(req)
+	require.NoError(t, err)
+
+	err = ValidateRequest(context.Background(), &RequestValidationInput{
+		Request:    req,
+		PathParams: pathParams,
+		Route:      route,
+		Options: &Options{
+			ExcludeRequestQueryParams: true,
+		},
+	})
+	require.NoError(t, err)
+
+	err = ValidateRequest(context.Background(), &RequestValidationInput{
+		Request:    req,
+		PathParams: pathParams,
+		Route:      route,
+		Options: &Options{
+			ExcludeRequestQueryParams: false,
+		},
+	})
+	require.Error(t, err)
+}