Repository: getkin/kin-openapi (go)

Issue:
Validation of `multipart/form-data` request fails for schemas containing `allOf`
Hello,

I get an error when validating a request with the following Spec

```yaml
openapi: 3.0.0
info:
  title: 'Validator'
  version: 0.0.1
paths:
  /test:
    post:
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              allOf:
              - $ref: '#/components/schemas/Category'
              - properties:
                  file:
                    type: string
                    format: binary
                  description:
                    type: string
      responses:
        '200':
          description: Created

components:
  schemas:
    Category:
      type: object
      properties:
        name:
          type: string
      required:
      - name
```

Depending on the request itself, the following error occurs at the very first boundary.

```
request body has an error: failed to decode request body: part file: undefined
```

Pull request description:
openapi3filter: support for allOf request schema in multipart/form-data
fix #722

No support for anyOf schema is provided.

Patch:
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index 4791b4a..2cd700c 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -1120,33 +1120,47 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
 			enc = encFn(name)
 		}
 		subEncFn := func(string) *openapi3.Encoding { return enc }
-		// If the property's schema has type "array" it is means that the form contains a few parts with the same name.
-		// Every such part has a type that is defined by an items schema in the property's schema.
+
 		var valueSchema *openapi3.SchemaRef
-		var exists bool
-		valueSchema, exists = schema.Value.Properties[name]
-		if !exists {
-			anyProperties := schema.Value.AdditionalPropertiesAllowed
-			if anyProperties != nil {
-				switch *anyProperties {
-				case true:
-					//additionalProperties: true
-					continue
-				default:
-					//additionalProperties: false
-					return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+		if len(schema.Value.AllOf) > 0 {
+			var exists bool
+			for _, sr := range schema.Value.AllOf {
+				valueSchema, exists = sr.Value.Properties[name]
+				if exists {
+					break
 				}
 			}
-			if schema.Value.AdditionalProperties == nil {
+			if !exists {
 				return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
 			}
-			valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name]
+		} else {
+			// If the property's schema has type "array" it is means that the form contains a few parts with the same name.
+			// Every such part has a type that is defined by an items schema in the property's schema.
+			var exists bool
+			valueSchema, exists = schema.Value.Properties[name]
 			if !exists {
-				return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+				anyProperties := schema.Value.AdditionalPropertiesAllowed
+				if anyProperties != nil {
+					switch *anyProperties {
+					case true:
+						//additionalProperties: true
+						continue
+					default:
+						//additionalProperties: false
+						return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+					}
+				}
+				if schema.Value.AdditionalProperties == nil {
+					return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+				}
+				valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name]
+				if !exists {
+					return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
+				}
+			}
+			if valueSchema.Value.Type == "array" {
+				valueSchema = valueSchema.Value.Items
 			}
-		}
-		if valueSchema.Value.Type == "array" {
-			valueSchema = valueSchema.Value.Items
 		}
 
 		var value interface{}
@@ -1160,14 +1174,28 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
 	}
 
 	allTheProperties := make(map[string]*openapi3.SchemaRef)
-	for k, v := range schema.Value.Properties {
-		allTheProperties[k] = v
-	}
-	if schema.Value.AdditionalProperties != nil {
-		for k, v := range schema.Value.AdditionalProperties.Value.Properties {
+	if len(schema.Value.AllOf) > 0 {
+		for _, sr := range schema.Value.AllOf {
+			for k, v := range sr.Value.Properties {
+				allTheProperties[k] = v
+			}
+			if sr.Value.AdditionalProperties != nil {
+				for k, v := range sr.Value.AdditionalProperties.Value.Properties {
+					allTheProperties[k] = v
+				}
+			}
+		}
+	} else {
+		for k, v := range schema.Value.Properties {
 			allTheProperties[k] = v
 		}
+		if schema.Value.AdditionalProperties != nil {
+			for k, v := range schema.Value.AdditionalProperties.Value.Properties {
+				allTheProperties[k] = v
+			}
+		}
 	}
+
 	// Make an object value from form values.
 	obj := make(map[string]interface{})
 	for name, prop := range allTheProperties {


Test patch:
diff --git a/openapi3filter/issue722_test.go b/openapi3filter/issue722_test.go
new file mode 100644
index 0000000..2ffa9d1
--- /dev/null
+++ b/openapi3filter/issue722_test.go
@@ -0,0 +1,133 @@
+package openapi3filter_test
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"net/textproto"
+	"strings"
+	"testing"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestValidateMultipartFormDataContainingAllOf(t *testing.T) {
+	const spec = `
+openapi: 3.0.0
+info:
+  title: 'Validator'
+  version: 0.0.1
+paths:
+  /test:
+    post:
+      requestBody:
+        required: true
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              required:
+                - file
+              allOf:
+              - $ref: '#/components/schemas/Category'
+              - properties:
+                  file:
+                    type: string
+                    format: binary
+                  description:
+                    type: string
+      responses:
+        '200':
+          description: Created
+
+components:
+  schemas:
+    Category:
+      type: object
+      properties:
+        name:
+          type: string
+      required:
+      - name
+`
+
+	loader := openapi3.NewLoader()
+	doc, err := loader.LoadFromData([]byte(spec))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err = doc.Validate(loader.Context); err != nil {
+		t.Fatal(err)
+	}
+
+	router, err := gorillamux.NewRouter(doc)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	body := &bytes.Buffer{}
+	writer := multipart.NewWriter(body)
+
+	{ // Add file data
+		fw, err := writer.CreateFormFile("file", "hello.txt")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	{ // Add a single "name" item as part data
+		h := make(textproto.MIMEHeader)
+		h.Set("Content-Disposition", `form-data; name="name"`)
+		fw, err := writer.CreatePart(h)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if _, err = io.Copy(fw, strings.NewReader(`foo`)); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	{ // Add a single "discription" item as part data
+		h := make(textproto.MIMEHeader)
+		h.Set("Content-Disposition", `form-data; name="description"`)
+		fw, err := writer.CreatePart(h)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if _, err = io.Copy(fw, strings.NewReader(`description note`)); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	writer.Close()
+
+	req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
+	if err != nil {
+		t.Fatal(err)
+	}
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	route, pathParams, err := router.FindRoute(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err = openapi3filter.ValidateRequestBody(
+		context.Background(),
+		&openapi3filter.RequestValidationInput{
+			Request:    req,
+			PathParams: pathParams,
+			Route:      route,
+		},
+		route.Operation.RequestBody.Value,
+	); err != nil {
+		t.Error(err)
+	}
+}