Repository: swaggest/rest
Pull request #2 (2020-11-01 21:01:36)

Add support for dynamic uploads
![image](https://user-images.githubusercontent.com/1381436/97815382-e3a2dd00-1c8d-11eb-827f-1b2dba2a7ef8.png)


codecov-io: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=h1) Report
> Merging [#2](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=desc) into [master](https://codecov.io/gh/swaggest/rest/commit/3538975202e3aa50691c9165548091ecc98b5484?el=desc) will **increase** coverage by `0.08%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/swaggest/rest/pull/2/graphs/tree.svg?width=650&height=150&src=pr&token=OpLHxxPdmn)](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master       #2      +/-   ##
==========================================
+ Coverage   69.02%   69.11%   +0.08%     
==========================================
  Files          25       25              
  Lines        1298     1308      +10     
==========================================
+ Hits          896      904       +8     
- Misses        306      307       +1     
- Partials       96       97       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `69.11% <83.33%> (+0.08%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [request/file.go](https://codecov.io/gh/swaggest/rest/pull/2/diff?src=pr&el=tree#diff-cmVxdWVzdC9maWxlLmdv) | `65.11% <80.00%> (+3.35%)` | :arrow_up: |
| [request/reflect.go](https://codecov.io/gh/swaggest/rest/pull/2/diff?src=pr&el=tree#diff-cmVxdWVzdC9yZWZsZWN0Lmdv) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=footer). Last update [3538975...9579909](https://codecov.io/gh/swaggest/rest/pull/2?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/_examples/advanced/file_multi_upload.go b/_examples/advanced/file_multi_upload.go
new file mode 100644
index 0000000..d98a6a7
--- /dev/null
+++ b/_examples/advanced/file_multi_upload.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+	"context"
+	"mime/multipart"
+	"net/textproto"
+
+	"github.com/swaggest/usecase"
+)
+
+func fileMultiUploader() usecase.Interactor {
+	u := struct {
+		usecase.Interactor
+		usecase.Info
+		usecase.WithInput
+		usecase.WithOutput
+	}{}
+
+	u.SetTitle("Files Uploads With 'multipart/form-data'")
+
+	type upload struct {
+		Simple   string                  `formData:"simple" description:"Simple scalar value in body."`
+		Query    int                     `query:"in_query" description:"Simple scalar value in query."`
+		Uploads1 []*multipart.FileHeader `formData:"uploads1" description:"Uploads with *multipart.FileHeader."`
+		Uploads2 []multipart.File        `formData:"uploads2" description:"Uploads with multipart.File."`
+	}
+
+	type info struct {
+		Filenames    []string               `json:"filenames"`
+		Headers      []textproto.MIMEHeader `json:"headers"`
+		Sizes        []int64                `json:"sizes"`
+		Upload1Peeks []string               `json:"peeks1"`
+		Upload2Peeks []string               `json:"peeks2"`
+		Simple       string                 `json:"simple"`
+		Query        int                    `json:"inQuery"`
+	}
+
+	u.Input = new(upload)
+	u.Output = new(info)
+
+	u.Interactor = usecase.Interact(func(ctx context.Context, input, output interface{}) (err error) {
+		var (
+			in  = input.(*upload)
+			out = output.(*info)
+		)
+
+		out.Query = in.Query
+		out.Simple = in.Simple
+		for _, o := range in.Uploads1 {
+			out.Filenames = append(out.Filenames, o.Filename)
+			out.Headers = append(out.Headers, o.Header)
+			out.Sizes = append(out.Sizes, o.Size)
+
+			f, err := o.Open()
+			if err != nil {
+				return err
+			}
+			p := make([]byte, 100)
+			_, err = f.Read(p)
+			if err != nil {
+				return err
+			}
+
+			out.Upload1Peeks = append(out.Upload1Peeks, string(p))
+
+			err = f.Close()
+			if err != nil {
+				return err
+			}
+		}
+
+		for _, o := range in.Uploads2 {
+			p := make([]byte, 100)
+			_, err = o.Read(p)
+			if err != nil {
+				return err
+			}
+
+			out.Upload2Peeks = append(out.Upload2Peeks, string(p))
+			err = o.Close()
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+
+	return u
+}
diff --git a/_examples/advanced/router.go b/_examples/advanced/router.go
index 50c37cb..cdad4fb 100644
--- a/_examples/advanced/router.go
+++ b/_examples/advanced/router.go
@@ -56,6 +56,7 @@ func NewRouter() http.Handler {
 	r.Method(http.MethodGet, "/query-object", nethttp.NewHandler(queryObject()))
 
 	r.Method(http.MethodPost, "/file-upload", nethttp.NewHandler(fileUploader()))
+	r.Method(http.MethodPost, "/file-multi-upload", nethttp.NewHandler(fileMultiUploader()))
 	r.Method(http.MethodPost, "/json-body/{in-path}", nethttp.NewHandler(jsonBody()))
 	r.Method(http.MethodPost, "/json-body-validation/{in-path}", nethttp.NewHandler(jsonBodyValidation()))
 	r.Method(http.MethodPost, "/json-slice-body", nethttp.NewHandler(jsonSliceBody()))
diff --git a/_examples/go.mod b/_examples/go.mod
index 3916446..d64c642 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -6,7 +6,7 @@ replace github.com/swaggest/rest => ../
 
 require (
 	github.com/andybalholm/brotli v1.0.1 // indirect
-	github.com/bool64/dev v0.1.5
+	github.com/bool64/dev v0.1.6
 	github.com/bool64/httptestbench v0.1.0
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/kelseyhightower/envconfig v1.4.0
@@ -14,7 +14,7 @@ require (
 	github.com/stretchr/testify v1.6.1
 	github.com/swaggest/assertjson v1.3.0
 	github.com/swaggest/jsonschema-go v0.3.11
-	github.com/swaggest/openapi-go v0.2.2
+	github.com/swaggest/openapi-go v0.2.3
 	github.com/swaggest/rest v0.0.0-20201021182948-25d31f7f38d4
 	github.com/swaggest/swgui v1.0.9
 	github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8
diff --git a/_examples/go.sum b/_examples/go.sum
index 8c507a4..c63ffba 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -2,12 +2,11 @@ github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu
 github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
-github.com/bool64/dev v0.1.2 h1:FIwpKlQQoFrGVXkYxLA9X2Kq2B2GXRBEKpcYo+O4NFw=
 github.com/bool64/dev v0.1.2/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
-github.com/bool64/dev v0.1.3 h1:qi6s5H6iUkKwGIuLyeZLLe3ZAYPY+mWo1Nsa2p4LI+o=
 github.com/bool64/dev v0.1.3/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
-github.com/bool64/dev v0.1.5 h1:35/UOPpbQE2ol+amR3UCb9GtKgjbjPLGNX8Kpa5nqpQ=
 github.com/bool64/dev v0.1.5/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
+github.com/bool64/dev v0.1.6 h1:QP8buknknKrPVizPbOKeyCt6/2+iZ309wxjreckowVY=
+github.com/bool64/dev v0.1.6/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/bool64/httptestbench v0.1.0 h1:Nr+9KjZbrlgmd8ABGfFvEYEnMdbAWxj13YHeQR6OOzA=
 github.com/bool64/httptestbench v0.1.0/go.mod h1:xPlusBpnquKisPwqjpgK8fBPwa0q1r+ecFoWxRtNjgU=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
@@ -26,7 +25,6 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
 github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
 github.com/iancoleman/orderedmap v0.1.0 h1:2orAxZBJsvimgEBmMWfXaFlzSG2fbQil5qzP3F6cCkg=
 github.com/iancoleman/orderedmap v0.1.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
@@ -81,16 +79,16 @@ github.com/swaggest/form v3.6.4+incompatible h1:K0EaZjf8C7cScuZgnJG+Sg0GUGAtjPZi
 github.com/swaggest/form v3.6.4+incompatible/go.mod h1:eTJPpSayjnJlOQQbDYsSHlbREbSAtudNW/Y6HouVWr0=
 github.com/swaggest/jsonschema-go v0.2.1/go.mod h1:QFauBdPTrU1UltwocM5FzOWnVjVVtcWkJWG3NlK9sV0=
 github.com/swaggest/jsonschema-go v0.2.4/go.mod h1:m4VV88Gbi7lCrt9ckJzigK1rMlEeFjdZUkJr1o5MnDE=
-github.com/swaggest/jsonschema-go v0.3.8 h1:QkRQXoO1jp9otq5i0blji6T2gVbsF0OfdiMHFuKeUwM=
 github.com/swaggest/jsonschema-go v0.3.8/go.mod h1:AQijowS82ZcUId9RStqvyGTxyhzYUiVDxelwQxEdOy8=
 github.com/swaggest/jsonschema-go v0.3.11 h1:rbYBJhAV2Uz1PB4iJyKbvzGO+o1FsdX60aLi+o67IHU=
 github.com/swaggest/jsonschema-go v0.3.11/go.mod h1:lJUbAc2E3OpGrrUGvayONeRBXsFJeedWTpm23jhOtKs=
 github.com/swaggest/openapi-go v0.1.3/go.mod h1:Zx4ZgJ7XvlFH9wCOHE7u8RAjLfiHAnCHeaD5kUDujVM=
 github.com/swaggest/openapi-go v0.2.2 h1:ZV6tjpR/W97sIOknkaOAt/U2FpqoEptRZrYiksfNKkY=
 github.com/swaggest/openapi-go v0.2.2/go.mod h1:GyvI08XD7gnTZxFteI2/Rnk6hgmcSufqG8wVBz/yzqo=
+github.com/swaggest/openapi-go v0.2.3 h1:DsbY56dzJQzKLjoJKl15WtykoyF1OJrqIzUUH+ovTDg=
+github.com/swaggest/openapi-go v0.2.3/go.mod h1:R4cj4xlxiODL8m+x0pKbWXRZYx67UTYOx0xfH0zxm3k=
 github.com/swaggest/refl v0.1.0/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.2/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
-github.com/swaggest/refl v0.1.3 h1:cnzEBcCNhYeLPG8Yy9JQixUkxMDsF0mo0GyzblLWrjE=
 github.com/swaggest/refl v0.1.3/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.5 h1:dsIhZmcVkNqsnnc4SZO7ezp7j8cTn+N/i8ND+0WxA/I=
 github.com/swaggest/refl v0.1.5/go.mod h1:qeng4aRKeRt8NYAuLpag+FY9A/pDMkSwe7zhXn6/LW4=
@@ -120,7 +118,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
 golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
diff --git a/go.mod b/go.mod
index 43023c7..e11ea27 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/swaggest/rest
 go 1.13
 
 require (
-	github.com/bool64/dev v0.1.5
+	github.com/bool64/dev v0.1.6
 	github.com/cespare/xxhash/v2 v2.1.1
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/iancoleman/orderedmap v0.1.0 // indirect
@@ -11,8 +11,7 @@ require (
 	github.com/stretchr/testify v1.6.1
 	github.com/swaggest/assertjson v1.3.0
 	github.com/swaggest/form v3.6.4+incompatible
-	github.com/swaggest/jsonschema-go v0.3.11 // indirect
-	github.com/swaggest/openapi-go v0.2.2
+	github.com/swaggest/openapi-go v0.2.3
 	github.com/swaggest/refl v0.1.5
 	github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8
 	golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
diff --git a/go.sum b/go.sum
index 6a7d6cb..6504622 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,8 @@
-github.com/bool64/dev v0.1.0 h1:erHyUcwsuvZU8YvydZuZDCPR6CeQXALfiVl4lx7TW7o=
 github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
-github.com/bool64/dev v0.1.2 h1:FIwpKlQQoFrGVXkYxLA9X2Kq2B2GXRBEKpcYo+O4NFw=
 github.com/bool64/dev v0.1.2/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
-github.com/bool64/dev v0.1.5 h1:35/UOPpbQE2ol+amR3UCb9GtKgjbjPLGNX8Kpa5nqpQ=
 github.com/bool64/dev v0.1.5/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
+github.com/bool64/dev v0.1.6 h1:QP8buknknKrPVizPbOKeyCt6/2+iZ309wxjreckowVY=
+github.com/bool64/dev v0.1.6/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -20,7 +19,6 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
 github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
 github.com/iancoleman/orderedmap v0.1.0 h1:2orAxZBJsvimgEBmMWfXaFlzSG2fbQil5qzP3F6cCkg=
 github.com/iancoleman/orderedmap v0.1.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
@@ -60,7 +58,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/swaggest/assertjson v1.0.0/go.mod h1:mE5ltBbrB+Ya8Xar5OMITxya76vwLZMoPG8aXF7sVQc=
-github.com/swaggest/assertjson v1.2.0 h1:5o3xwits8nn2xw3bDpH80CN4YHJUMuVVNh4HO1azuls=
 github.com/swaggest/assertjson v1.2.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
 github.com/swaggest/assertjson v1.3.0 h1:751J+Plmg68cDGeMwMIq5nEvBw8g+ErG4UMcv9kpHmk=
 github.com/swaggest/assertjson v1.3.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
@@ -68,16 +65,13 @@ github.com/swaggest/form v3.6.4+incompatible h1:K0EaZjf8C7cScuZgnJG+Sg0GUGAtjPZi
 github.com/swaggest/form v3.6.4+incompatible/go.mod h1:eTJPpSayjnJlOQQbDYsSHlbREbSAtudNW/Y6HouVWr0=
 github.com/swaggest/jsonschema-go v0.2.1/go.mod h1:QFauBdPTrU1UltwocM5FzOWnVjVVtcWkJWG3NlK9sV0=
 github.com/swaggest/jsonschema-go v0.2.4/go.mod h1:m4VV88Gbi7lCrt9ckJzigK1rMlEeFjdZUkJr1o5MnDE=
-github.com/swaggest/jsonschema-go v0.3.8 h1:QkRQXoO1jp9otq5i0blji6T2gVbsF0OfdiMHFuKeUwM=
-github.com/swaggest/jsonschema-go v0.3.8/go.mod h1:AQijowS82ZcUId9RStqvyGTxyhzYUiVDxelwQxEdOy8=
 github.com/swaggest/jsonschema-go v0.3.11 h1:rbYBJhAV2Uz1PB4iJyKbvzGO+o1FsdX60aLi+o67IHU=
 github.com/swaggest/jsonschema-go v0.3.11/go.mod h1:lJUbAc2E3OpGrrUGvayONeRBXsFJeedWTpm23jhOtKs=
 github.com/swaggest/openapi-go v0.1.3/go.mod h1:Zx4ZgJ7XvlFH9wCOHE7u8RAjLfiHAnCHeaD5kUDujVM=
-github.com/swaggest/openapi-go v0.2.2 h1:ZV6tjpR/W97sIOknkaOAt/U2FpqoEptRZrYiksfNKkY=
-github.com/swaggest/openapi-go v0.2.2/go.mod h1:GyvI08XD7gnTZxFteI2/Rnk6hgmcSufqG8wVBz/yzqo=
+github.com/swaggest/openapi-go v0.2.3 h1:DsbY56dzJQzKLjoJKl15WtykoyF1OJrqIzUUH+ovTDg=
+github.com/swaggest/openapi-go v0.2.3/go.mod h1:R4cj4xlxiODL8m+x0pKbWXRZYx67UTYOx0xfH0zxm3k=
 github.com/swaggest/refl v0.1.0/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.2/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
-github.com/swaggest/refl v0.1.3 h1:cnzEBcCNhYeLPG8Yy9JQixUkxMDsF0mo0GyzblLWrjE=
 github.com/swaggest/refl v0.1.3/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.5 h1:dsIhZmcVkNqsnnc4SZO7ezp7j8cTn+N/i8ND+0WxA/I=
 github.com/swaggest/refl v0.1.5/go.mod h1:qeng4aRKeRt8NYAuLpag+FY9A/pDMkSwe7zhXn6/LW4=
@@ -117,7 +111,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/request/file.go b/request/file.go
index 730bf24..0bd2e38 100644
--- a/request/file.go
+++ b/request/file.go
@@ -11,8 +11,10 @@ import (
 )
 
 var (
-	multipartFileType       = reflect.TypeOf((*multipart.File)(nil)).Elem()
-	multipartFileHeaderType = reflect.TypeOf((*multipart.FileHeader)(nil))
+	multipartFileType        = reflect.TypeOf((*multipart.File)(nil)).Elem()
+	multipartFilesType       = reflect.TypeOf(([]multipart.File)(nil))
+	multipartFileHeaderType  = reflect.TypeOf((*multipart.FileHeader)(nil))
+	multipartFileHeadersType = reflect.TypeOf(([]*multipart.FileHeader)(nil))
 )
 
 func decodeFiles(r *http.Request, input interface{}, _ rest.Validator) error {
@@ -35,7 +37,8 @@ func decodeFilesInStruct(r *http.Request, v reflect.Value) error {
 	for i := 0; i < t.NumField(); i++ {
 		field := t.Field(i)
 
-		if field.Type == multipartFileType || field.Type == multipartFileHeaderType {
+		if field.Type == multipartFileType || field.Type == multipartFileHeaderType ||
+			field.Type == multipartFilesType || field.Type == multipartFileHeadersType {
 			err := setFile(r, field, v.Field(i))
 			if err != nil {
 				return err
@@ -85,5 +88,24 @@ func setFile(r *http.Request, field reflect.StructField, v reflect.Value) error
 		v.Set(reflect.ValueOf(header))
 	}
 
+	if field.Type == multipartFilesType {
+		res := make([]multipart.File, 0, len(r.MultipartForm.File[name]))
+
+		for _, h := range r.MultipartForm.File[name] {
+			f, err := h.Open()
+			if err != nil {
+				return fmt.Errorf("failed to open uploaded file %s (%s): %w", name, h.Filename, err)
+			}
+
+			res = append(res, f)
+		}
+
+		v.Set(reflect.ValueOf(res))
+	}
+
+	if field.Type == multipartFileHeadersType {
+		v.Set(reflect.ValueOf(r.MultipartForm.File[name]))
+	}
+
 	return nil
 }
diff --git a/request/reflect.go b/request/reflect.go
index 0b7f3a5..428dc9d 100644
--- a/request/reflect.go
+++ b/request/reflect.go
@@ -11,7 +11,8 @@ func hasFileFields(i interface{}, tagname string) bool {
 	found := false
 
 	refl.WalkTaggedFields(reflect.ValueOf(i), func(v reflect.Value, sf reflect.StructField, tag string) {
-		if sf.Type == multipartFileType || sf.Type == multipartFileHeaderType {
+		if sf.Type == multipartFileType || sf.Type == multipartFileHeaderType ||
+			sf.Type == multipartFilesType || sf.Type == multipartFileHeadersType {
 			found = true
 
 			return


Test patch:
diff --git a/_examples/advanced/_testdata/openapi.json b/_examples/advanced/_testdata/openapi.json
index b5e1228..93ff9f2 100644
--- a/_examples/advanced/_testdata/openapi.json
+++ b/_examples/advanced/_testdata/openapi.json
@@ -2,6 +2,24 @@
   "openapi":"3.0.2",
   "info":{"title":"Advanced Example","description":"This app showcases a variety of features.","version":"v1.2.3"},
   "paths":{
+    "/file-multi-upload":{
+      "post":{
+        "summary":"Files Uploads With 'multipart/form-data'","description":"","operationId":"",
+        "parameters":[
+          {
+            "name":"in_query","in":"query","description":"Simple scalar value in query.",
+            "schema":{"type":"integer","description":"Simple scalar value in query."}
+          }
+        ],
+        "requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedUploadType2"}}}},
+        "responses":{
+          "200":{
+            "description":"OK",
+            "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfoType2"}}}
+          }
+        }
+      }
+    },
     "/file-upload":{
       "post":{
         "summary":"File Upload With 'multipart/form-data'","description":"","operationId":"",
@@ -299,6 +317,16 @@
           "size":{"type":"integer"}
         }
       },
+      "AdvancedInfoType2":{
+        "type":"object",
+        "properties":{
+          "filenames":{"type":"array","items":{"type":"string"},"nullable":true},
+          "headers":{"type":"array","items":{"$ref":"#/components/schemas/TextprotoMIMEHeader"},"nullable":true},
+          "inQuery":{"type":"integer"},"peeks1":{"type":"array","items":{"type":"string"},"nullable":true},
+          "peeks2":{"type":"array","items":{"type":"string"},"nullable":true},"simple":{"type":"string"},
+          "sizes":{"type":"array","items":{"type":"integer"},"nullable":true}
+        }
+      },
       "AdvancedInputPortType2":{
         "required":["data"],"type":"object",
         "properties":{
@@ -360,8 +388,22 @@
           "upload2":{"$ref":"#/components/schemas/FormDataMultipartFile"}
         }
       },
+      "FormDataAdvancedUploadType2":{
+        "type":"object",
+        "properties":{
+          "simple":{"type":"string","description":"Simple scalar value in body."},
+          "uploads1":{
+            "type":"array","items":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"},
+            "description":"Uploads with *multipart.FileHeader.","nullable":true
+          },
+          "uploads2":{
+            "type":"array","items":{"$ref":"#/components/schemas/FormDataMultipartFile"},
+            "description":"Uploads with multipart.File.","nullable":true
+          }
+        }
+      },
       "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
-      "FormDataMultipartFileHeader":{"type":"string","format":"binary","nullable":true},
+      "FormDataMultipartFileHeader":{"type":"string","format":"binary"},
       "RestErrResponse":{
         "type":"object",
         "properties":{
@@ -371,7 +413,7 @@
           "status":{"type":"string","description":"Status text."}
         }
       },
-      "TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"nullable":true}
+      "TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}
     }
   }
 }
diff --git a/request/file_test.go b/request/file_test.go
index 33be70b..a900674 100644
--- a/request/file_test.go
+++ b/request/file_test.go
@@ -22,12 +22,14 @@ import (
 )
 
 type ReqEmb struct {
-	UploadHeader *multipart.FileHeader `formData:"upload"`
+	UploadHeader   *multipart.FileHeader   `formData:"upload"`
+	UploadsHeaders []*multipart.FileHeader `formData:"uploads"`
 }
 
 type fileReqTest struct {
 	ReqEmb
-	Upload multipart.File `file:"upload"`
+	Upload  multipart.File   `file:"upload"`
+	Uploads []multipart.File `formData:"uploads"`
 }
 
 func TestMapper_Decode_fileUploadTag(t *testing.T) {
@@ -52,16 +54,34 @@ func TestMapper_Decode_fileUploadTag(t *testing.T) {
 
 	u.Input = new(fileReqTest)
 	u.Interactor = usecase.Interact(func(ctx context.Context, input, output interface{}) error {
-		inp, ok := input.(*fileReqTest)
+		in, ok := input.(*fileReqTest)
 		assert.True(t, ok)
-		assert.NotNil(t, inp.Upload)
-		assert.NotNil(t, inp.UploadHeader)
-		assert.Equal(t, "my.csv", inp.UploadHeader.Filename)
-		assert.Equal(t, int64(6), inp.UploadHeader.Size)
-		content, err := ioutil.ReadAll(inp.Upload)
+		assert.NotNil(t, in.Upload)
+		assert.NotNil(t, in.UploadHeader)
+		assert.Equal(t, "my.csv", in.UploadHeader.Filename)
+		assert.Equal(t, int64(6), in.UploadHeader.Size)
+		content, err := ioutil.ReadAll(in.Upload)
 		assert.NoError(t, err)
+		assert.NoError(t, in.Upload.Close())
 		assert.Equal(t, "Hello!", string(content))
 
+		assert.Len(t, in.Uploads, 2)
+		assert.Len(t, in.UploadsHeaders, 2)
+		assert.Equal(t, "my1.csv", in.UploadsHeaders[0].Filename)
+		assert.Equal(t, int64(7), in.UploadsHeaders[0].Size)
+		assert.Equal(t, "my2.csv", in.UploadsHeaders[1].Filename)
+		assert.Equal(t, int64(7), in.UploadsHeaders[1].Size)
+
+		content, err = ioutil.ReadAll(in.Uploads[0])
+		assert.NoError(t, err)
+		assert.NoError(t, in.Uploads[0].Close())
+		assert.Equal(t, "Hello1!", string(content))
+
+		content, err = ioutil.ReadAll(in.Uploads[1])
+		assert.NoError(t, err)
+		assert.NoError(t, in.Uploads[1].Close())
+		assert.Equal(t, "Hello2!", string(content))
+
 		return nil
 	})
 
@@ -80,6 +100,18 @@ func TestMapper_Decode_fileUploadTag(t *testing.T) {
 	_, err = writer.Write([]byte(`Hello!`))
 	assert.NoError(t, err)
 
+	writer, err = w.CreateFormFile("uploads", "my1.csv")
+	assert.NoError(t, err)
+
+	_, err = writer.Write([]byte(`Hello1!`))
+	assert.NoError(t, err)
+
+	writer, err = w.CreateFormFile("uploads", "my2.csv")
+	assert.NoError(t, err)
+
+	_, err = writer.Write([]byte(`Hello2!`))
+	assert.NoError(t, err)
+
 	assert.NoError(t, w.Close())
 
 	hreq := httptest.NewRequest(http.MethodPost, srv.URL+"/receive", &b)
diff --git a/request/jsonbody_test.go b/request/jsonbody_test.go
index 44b25a3..5e1943f 100644
--- a/request/jsonbody_test.go
+++ b/request/jsonbody_test.go
@@ -71,7 +71,7 @@ func Test_decodeJSONBody_decodeFailed(t *testing.T) {
 	var i []int
 
 	err = decodeJSONBody(req, &i, nil)
-	assert.EqualError(t, err, "failed to decode json: invalid character 'a' looking for beginning of value")
+	assert.Error(t, err)
 }
 
 func Test_decodeJSONBody_unmarshalFailed(t *testing.T) {