Repository: ogen-go/ogen
Pull request #403 (2022-06-17 12:49:31)

feat(gen): support optional files and file arrays


Patch:
diff --git a/gen/_template/request_decode.tmpl b/gen/_template/request_decode.tmpl
index 24424e2b..042b4100 100644
--- a/gen/_template/request_decode.tmpl
+++ b/gen/_template/request_decode.tmpl
@@ -136,26 +136,9 @@ func (s *Server) decode{{ $op.Name }}Request(r *http.Request, span trace.Span) (
 		{{- if $contentType.MultipartForm }}
         {{- range $param := $op.Request.FileParameters $contentType }}
 		if err := func() error {
-			files, ok := r.MultipartForm.File[{{ quote $param.Spec.Name }}]
-			if !ok || len(files) < 1 {
-				return errors.New("file is not set")
-			}
-			fh := files[0]
-
-			f, err := fh.Open()
-			if err != nil {
-				return errors.Wrap(err, "open")
-			}
-			closers = append(closers, f)
-
-        	{{ printf "req.%s" $param.Name }} = ht.MultipartFile{
-				Name:   fh.Filename,
-				File:   f,
-				Header: fh.Header,
-			}
-			return nil
+			{{- template "decode_multipart_file_param" $param }}
 		}(); err != nil {
-			return req, close, errors.Wrap(err, {{ printf "file %q" $param.Spec.Name | quote }})
+			return req, close, errors.Wrap(err, {{ printf "decode %q" $param.Spec.Name | quote }})
 		}
 		{{- end }}
 		{{- end }}
@@ -172,3 +155,67 @@ func (s *Server) decode{{ $op.Name }}Request(r *http.Request, span trace.Span) (
 {{- end }}
 {{ end }}
 {{ end }}
+
+{{- define "decode_multipart_file_param" }}
+{{- /*gotype: github.com/ogen-go/ogen/gen/ir.Parameter*/ -}}
+{{- $name := quote $.Spec.Name }}
+{{- $t := $.Type }}
+	files, ok := r.MultipartForm.File[{{ $name }}]
+	{{- if or $t.IsPrimitive $t.IsGeneric }}
+	if !ok || len(files) < 1  {
+		{{- if $t.IsPrimitive }}
+		return validate.ErrFieldRequired
+		{{- else }}
+		return nil
+		{{- end }}
+	}
+	fh := files[0]
+
+	f, err := fh.Open()
+	if err != nil {
+		return errors.Wrap(err, "open")
+	}
+	closers = append(closers, f)
+
+    {{- if $t.IsPrimitive }}
+	{{ printf "req.%s" $.Name }} = ht.MultipartFile{
+		Name:   fh.Filename,
+		File:   f,
+		Header: fh.Header,
+	}
+	{{- else }}
+	{{ printf "req.%s" $.Name }}.SetTo(ht.MultipartFile{
+		Name:   fh.Filename,
+		File:   f,
+		Header: fh.Header,
+	})
+	{{- end }}
+	{{- else if $t.IsArray }}
+	_ = ok
+	req.Files = make({{ $t.Go }}, 0, len(files))
+	for _, fh := range files {
+		f, err := fh.Open()
+		if err != nil {
+			return errors.Wrap(err, "open")
+		}
+		closers = append(closers, f)
+
+		{{ printf "req.%s" $.Name }} = append({{ printf "req.%s" $.Name }}, ht.MultipartFile{
+			Name:   fh.Filename,
+			File:   f,
+			Header: fh.Header,
+		})
+	}
+	{{- else }}
+		{{ errorf "unexpected kind %s" $t.Kind }}
+	{{- end }}
+
+	{{- if $t.NeedValidation }}
+	if err := func() error {
+		{{- template "validate" elem $t (printf "req.%s" $.Name) }}
+	}(); err != nil {
+		return errors.Wrap(err, "validate")
+	}
+	{{- end }}
+	return nil
+{{- end }}
diff --git a/gen/_template/request_encode.tmpl b/gen/_template/request_encode.tmpl
index 0368dbfc..c40cc840 100644
--- a/gen/_template/request_encode.tmpl
+++ b/gen/_template/request_encode.tmpl
@@ -51,9 +51,7 @@ func encode{{ $op.Name }}Request{{ $contentType.Name }}(
 		{{- else if $contentType.MultipartForm }}
 			getBody, contentType := ht.CreateMultipartBody(func(w *multipart.Writer) error {
             	{{- range $param := $op.Request.FileParameters $contentType }}
-				if err := {{ printf "req.%s" $param.Name }}.WriteMultipart({{ quote $param.Spec.Name }}, w); err != nil {
-					return errors.Wrap(err, {{ printf "write %q" $param.Spec.Name | quote }})
-				}
+				{{- template "encode_multipart_file_param" $param }}
 				{{- end }}
 				if err := q.WriteMultipart(w); err != nil {
 					return errors.Wrap(err, "write multipart")
@@ -71,3 +69,36 @@ func encode{{ $op.Name }}Request{{ $contentType.Name }}(
 {{- end }}
 {{- end }}{{ end }}
 {{ end }}
+
+{{- define "encode_multipart_file_param" }}
+{{- /*gotype: github.com/ogen-go/ogen/gen/ir.Parameter*/ -}}
+{{- $name := quote $.Spec.Name }}
+{{- $errWrite := printf "write %s" $name | quote }}
+{{- $recv := printf "req.%s" $.Name }}
+{{- $t := $.Type }}
+
+{{- if $t.IsPrimitive }}
+	if err := {{ $recv }}.WriteMultipart({{ $name }}, w); err != nil {
+		return errors.Wrap(err, {{ $errWrite }})
+	}
+{{- else if $t.IsGeneric }}
+	if val, ok := {{ $recv }}.Get(); ok {
+		if err := val.WriteMultipart({{ $name }}, w); err != nil {
+			return errors.Wrap(err, {{ $errWrite }})
+		}
+	}
+{{- else if $t.IsArray }}
+	if err := func() error {
+		for idx, val := range {{ $recv }} {
+			if err := val.WriteMultipart({{ $name }}, w); err != nil {
+				return errors.Wrapf(err, "file [%d]", idx)
+			}
+		}
+		return nil
+	}(); err != nil {
+		return errors.Wrap(err, {{ $errWrite }})
+	}
+{{- else }}
+    {{ errorf "unexpected kind %s" $t.Kind }}
+{{- end }}
+{{- end }}
diff --git a/gen/gen_contents.go b/gen/gen_contents.go
index 0f4f6c7b..50a3e519 100644
--- a/gen/gen_contents.go
+++ b/gen/gen_contents.go
@@ -167,11 +167,13 @@ func (g *Generator) generateContents(
 
 			case "multipart/form-data":
 				t, err := g.generateFormContent(ctx, typeName, media, optional, func(f *ir.Field) error {
-					if s := f.Spec; s != nil && isBinary(s.Schema) {
-						if !s.Required {
-							return &ErrNotImplemented{"optional multipart file"}
-						}
-						f.Type = ir.Primitive(ir.File, nil)
+					t, err := isMultipartFile(ctx, f.Type, f.Spec)
+					if err != nil {
+						return err
+					}
+					if t != nil {
+						t.AddFeature("multipart-file")
+						f.Type = t
 						return nil
 					}
 					f.Type.AddFeature("uri")
diff --git a/gen/ir/operation.go b/gen/ir/operation.go
index 667f2e7d..12e7d3f4 100644
--- a/gen/ir/operation.go
+++ b/gen/ir/operation.go
@@ -105,7 +105,7 @@ func (r Request) FormParameters(ct ContentType) (params []Parameter) {
 	}
 
 	for _, f := range t.Fields {
-		if t := f.Type; ct.MultipartForm() && t.IsPrimitive() && t.Primitive == File {
+		if t := f.Type; ct.MultipartForm() && t.HasFeature("multipart-file") {
 			continue
 		}
 		params = append(params, Parameter{
@@ -123,8 +123,12 @@ func (r Request) FileParameters(ct ContentType) (params []Parameter) {
 		panic(ct)
 	}
 
+	if !ct.MultipartForm() {
+		return params
+	}
+
 	for _, f := range t.Fields {
-		if t := f.Type; ct.MultipartForm() && t.IsPrimitive() && t.Primitive == File {
+		if t := f.Type; t.HasFeature("multipart-file") {
 			params = append(params, Parameter{
 				Name: f.Name,
 				Type: f.Type,
diff --git a/gen/utils.go b/gen/utils.go
index 41aed9f3..2bd3838a 100644
--- a/gen/utils.go
+++ b/gen/utils.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/ogen-go/ogen/gen/ir"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -20,6 +21,45 @@ func isBinary(s *jsonschema.Schema) bool {
 	}
 }
 
+// isMultipartFile tries to map field to multipart file.
+//
+// Returns nil type if field is not a file parameter.
+func isMultipartFile(ctx *genctx, t *ir.Type, p *jsonschema.Property) (*ir.Type, error) {
+	if p == nil || p.Schema == nil {
+		return nil, nil
+	}
+	file := ir.Primitive(ir.File, p.Schema)
+	switch {
+	case t.IsGeneric():
+		v := t.GenericVariant
+		if !isBinary(p.Schema) || !v.OnlyOptional() {
+			return nil, nil
+		}
+
+		r := ir.Generic("MultipartFile", file, v)
+		if err := ctx.saveType(r); err != nil {
+			return nil, err
+		}
+		return r, nil
+	case t.IsArray():
+		if !isBinary(p.Schema.Item) {
+			return nil, nil
+		}
+
+		r := ir.Array(file, ir.NilNull, p.Schema)
+		r.Validators = ir.Validators{
+			Array: t.Validators.Array,
+		}
+		return r, nil
+	case t.IsPrimitive():
+		if !isBinary(p.Schema) {
+			return nil, nil
+		}
+		return file, nil
+	}
+	return nil, nil
+}
+
 func statusText(code int) string {
 	r := http.StatusText(code)
 	if r != "" {
diff --git a/internal/sample_api/oas_client_gen.go b/internal/sample_api/oas_client_gen.go
index 3c9a9251..c9111ce9 100644
--- a/internal/sample_api/oas_client_gen.go
+++ b/internal/sample_api/oas_client_gen.go
@@ -1903,7 +1903,15 @@ func (c *Client) TestMultipart(ctx context.Context, request TestForm) (res TestM
 // TestMultipartUpload invokes testMultipartUpload operation.
 //
 // POST /testMultipartUpload
-func (c *Client) TestMultipartUpload(ctx context.Context, request TestMultipartUploadReq) (res string, err error) {
+func (c *Client) TestMultipartUpload(ctx context.Context, request TestMultipartUploadReq) (res TestMultipartUploadOK, err error) {
+	if err := func() error {
+		if err := request.Validate(); err != nil {
+			return err
+		}
+		return nil
+	}(); err != nil {
+		return res, errors.Wrap(err, "validate")
+	}
 	startTime := time.Now()
 	otelAttrs := []attribute.KeyValue{
 		otelogen.OperationID("testMultipartUpload"),
diff --git a/internal/sample_api/oas_faker_gen.go b/internal/sample_api/oas_faker_gen.go
index 37b25ebf..c844d96d 100644
--- a/internal/sample_api/oas_faker_gen.go
+++ b/internal/sample_api/oas_faker_gen.go
@@ -1405,6 +1405,32 @@ func (s *TestFloatValidation) SetFake() {
 	}
 }
 
+// SetFake set fake values.
+func (s *TestMultipartUploadOK) SetFake() {
+	{
+		{
+			s.File = "string"
+		}
+	}
+	{
+		{
+			s.OptionalFile.SetFake()
+		}
+	}
+	{
+		{
+			s.Files = nil
+			for i := 0; i < 0; i++ {
+				var elem string
+				{
+					elem = "string"
+				}
+				s.Files = append(s.Files, elem)
+			}
+		}
+	}
+}
+
 // SetFake set fake values.
 func (s *TestObjectQueryParameterOK) SetFake() {
 	{
diff --git a/internal/sample_api/oas_json_gen.go b/internal/sample_api/oas_json_gen.go
index 194c401c..9e9031c9 100644
--- a/internal/sample_api/oas_json_gen.go
+++ b/internal/sample_api/oas_json_gen.go
@@ -7530,6 +7530,150 @@ func (s *TestFloatValidation) UnmarshalJSON(data []byte) error {
 	return s.Decode(d)
 }
 
+// Encode implements json.Marshaler.
+func (s TestMultipartUploadOK) Encode(e *jx.Encoder) {
+	e.ObjStart()
+	s.encodeFields(e)
+	e.ObjEnd()
+}
+
+// encodeFields encodes fields.
+func (s TestMultipartUploadOK) encodeFields(e *jx.Encoder) {
+	{
+
+		e.FieldStart("file")
+		e.Str(s.File)
+	}
+	{
+		if s.OptionalFile.Set {
+			e.FieldStart("optional_file")
+			s.OptionalFile.Encode(e)
+		}
+	}
+	{
+
+		e.FieldStart("files")
+		e.ArrStart()
+		for _, elem := range s.Files {
+			e.Str(elem)
+		}
+		e.ArrEnd()
+	}
+}
+
+var jsonFieldsNameOfTestMultipartUploadOK = [3]string{
+	0: "file",
+	1: "optional_file",
+	2: "files",
+}
+
+// Decode decodes TestMultipartUploadOK from json.
+func (s *TestMultipartUploadOK) Decode(d *jx.Decoder) error {
+	if s == nil {
+		return errors.New("invalid: unable to decode TestMultipartUploadOK to nil")
+	}
+	var requiredBitSet [1]uint8
+
+	if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
+		switch string(k) {
+		case "file":
+			requiredBitSet[0] |= 1 << 0
+			if err := func() error {
+				v, err := d.Str()
+				s.File = string(v)
+				if err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"file\"")
+			}
+		case "optional_file":
+			if err := func() error {
+				s.OptionalFile.Reset()
+				if err := s.OptionalFile.Decode(d); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"optional_file\"")
+			}
+		case "files":
+			requiredBitSet[0] |= 1 << 2
+			if err := func() error {
+				s.Files = make([]string, 0)
+				if err := d.Arr(func(d *jx.Decoder) error {
+					var elem string
+					v, err := d.Str()
+					elem = string(v)
+					if err != nil {
+						return err
+					}
+					s.Files = append(s.Files, elem)
+					return nil
+				}); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"files\"")
+			}
+		default:
+			return d.Skip()
+		}
+		return nil
+	}); err != nil {
+		return errors.Wrap(err, "decode TestMultipartUploadOK")
+	}
+	// Validate required fields.
+	var failures []validate.FieldError
+	for i, mask := range [1]uint8{
+		0b00000101,
+	} {
+		if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
+			// Mask only required fields and check equality to mask using XOR.
+			//
+			// If XOR result is not zero, result is not equal to expected, so some fields are missed.
+			// Bits of fields which would be set are actually bits of missed fields.
+			missed := bits.OnesCount8(result)
+			for bitN := 0; bitN < missed; bitN++ {
+				bitIdx := bits.TrailingZeros8(result)
+				fieldIdx := i*8 + bitIdx
+				var name string
+				if fieldIdx < len(jsonFieldsNameOfTestMultipartUploadOK) {
+					name = jsonFieldsNameOfTestMultipartUploadOK[fieldIdx]
+				} else {
+					name = strconv.Itoa(fieldIdx)
+				}
+				failures = append(failures, validate.FieldError{
+					Name:  name,
+					Error: validate.ErrFieldRequired,
+				})
+				// Reset bit.
+				result &^= 1 << bitIdx
+			}
+		}
+	}
+	if len(failures) > 0 {
+		return &validate.Error{Fields: failures}
+	}
+
+	return nil
+}
+
+// MarshalJSON implements stdjson.Marshaler.
+func (s TestMultipartUploadOK) MarshalJSON() ([]byte, error) {
+	e := jx.Encoder{}
+	s.Encode(&e)
+	return e.Bytes(), nil
+}
+
+// UnmarshalJSON implements stdjson.Unmarshaler.
+func (s *TestMultipartUploadOK) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	return s.Decode(d)
+}
+
 // Encode implements json.Marshaler.
 func (s TestObjectQueryParameterOK) Encode(e *jx.Encoder) {
 	e.ObjStart()
diff --git a/internal/sample_api/oas_request_decoders_gen.go b/internal/sample_api/oas_request_decoders_gen.go
index ffc8e307..81c2c7d7 100644
--- a/internal/sample_api/oas_request_decoders_gen.go
+++ b/internal/sample_api/oas_request_decoders_gen.go
@@ -1088,9 +1088,9 @@ func (s *Server) decodeTestMultipartUploadRequest(r *http.Request, span trace.Sp
 			}
 		}
 		if err := func() error {
-			files, ok := r.MultipartForm.File["fileName"]
+			files, ok := r.MultipartForm.File["file"]
 			if !ok || len(files) < 1 {
-				return errors.New("file is not set")
+				return validate.ErrFieldRequired
 			}
 			fh := files[0]
 
@@ -1099,15 +1099,69 @@ func (s *Server) decodeTestMultipartUploadRequest(r *http.Request, span trace.Sp
 				return errors.Wrap(err, "open")
 			}
 			closers = append(closers, f)
+			req.File = ht.MultipartFile{
+				Name:   fh.Filename,
+				File:   f,
+				Header: fh.Header,
+			}
+			return nil
+		}(); err != nil {
+			return req, close, errors.Wrap(err, "decode \"file\"")
+		}
+		if err := func() error {
+			files, ok := r.MultipartForm.File["optional_file"]
+			if !ok || len(files) < 1 {
+				return nil
+			}
+			fh := files[0]
 
-			req.FileName = ht.MultipartFile{
+			f, err := fh.Open()
+			if err != nil {
+				return errors.Wrap(err, "open")
+			}
+			closers = append(closers, f)
+			req.OptionalFile.SetTo(ht.MultipartFile{
 				Name:   fh.Filename,
 				File:   f,
 				Header: fh.Header,
+			})
+			return nil
+		}(); err != nil {
+			return req, close, errors.Wrap(err, "decode \"optional_file\"")
+		}
+		if err := func() error {
+			files, ok := r.MultipartForm.File["files"]
+			_ = ok
+			req.Files = make([]ht.MultipartFile, 0, len(files))
+			for _, fh := range files {
+				f, err := fh.Open()
+				if err != nil {
+					return errors.Wrap(err, "open")
+				}
+				closers = append(closers, f)
+
+				req.Files = append(req.Files, ht.MultipartFile{
+					Name:   fh.Filename,
+					File:   f,
+					Header: fh.Header,
+				})
+			}
+			if err := func() error {
+				if err := (validate.Array{
+					MinLength:    0,
+					MinLengthSet: false,
+					MaxLength:    5,
+					MaxLengthSet: true,
+				}).ValidateLength(len(req.Files)); err != nil {
+					return errors.Wrap(err, "array")
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "validate")
 			}
 			return nil
 		}(); err != nil {
-			return req, close, errors.Wrap(err, "file \"fileName\"")
+			return req, close, errors.Wrap(err, "decode \"files\"")
 		}
 
 		return req, close, nil
diff --git a/internal/sample_api/oas_request_encoders_gen.go b/internal/sample_api/oas_request_encoders_gen.go
index ca473e4f..a6795dd2 100644
--- a/internal/sample_api/oas_request_encoders_gen.go
+++ b/internal/sample_api/oas_request_encoders_gen.go
@@ -407,8 +407,23 @@ func encodeTestMultipartUploadRequest(
 		}
 	}
 	getBody, contentType := ht.CreateMultipartBody(func(w *multipart.Writer) error {
-		if err := req.FileName.WriteMultipart("fileName", w); err != nil {
-			return errors.Wrap(err, "write \"fileName\"")
+		if err := req.File.WriteMultipart("file", w); err != nil {
+			return errors.Wrap(err, "write \"file\"")
+		}
+		if val, ok := req.OptionalFile.Get(); ok {
+			if err := val.WriteMultipart("optional_file", w); err != nil {
+				return errors.Wrap(err, "write \"optional_file\"")
+			}
+		}
+		if err := func() error {
+			for idx, val := range req.Files {
+				if err := val.WriteMultipart("files", w); err != nil {
+					return errors.Wrapf(err, "file [%d]", idx)
+				}
+			}
+			return nil
+		}(); err != nil {
+			return errors.Wrap(err, "write \"files\"")
 		}
 		if err := q.WriteMultipart(w); err != nil {
 			return errors.Wrap(err, "write multipart")
diff --git a/internal/sample_api/oas_response_decoders_gen.go b/internal/sample_api/oas_response_decoders_gen.go
index 4e3b8457..ce5c0949 100644
--- a/internal/sample_api/oas_response_decoders_gen.go
+++ b/internal/sample_api/oas_response_decoders_gen.go
@@ -1031,7 +1031,7 @@ func decodeTestMultipartResponse(resp *http.Response, span trace.Span) (res Test
 		return res, validate.UnexpectedStatusCode(resp.StatusCode)
 	}
 }
-func decodeTestMultipartUploadResponse(resp *http.Response, span trace.Span) (res string, err error) {
+func decodeTestMultipartUploadResponse(resp *http.Response, span trace.Span) (res TestMultipartUploadOK, err error) {
 	switch resp.StatusCode {
 	case 200:
 		match := func(pattern, value string) bool {
@@ -1051,11 +1051,9 @@ func decodeTestMultipartUploadResponse(resp *http.Response, span trace.Span) (re
 			}
 
 			d := jx.DecodeBytes(buf.Bytes())
-			var response string
+			var response TestMultipartUploadOK
 			if err := func() error {
-				v, err := d.Str()
-				response = string(v)
-				if err != nil {
+				if err := response.Decode(d); err != nil {
 					return err
 				}
 				return nil
diff --git a/internal/sample_api/oas_response_encoders_gen.go b/internal/sample_api/oas_response_encoders_gen.go
index 23cfd13a..6db735c8 100644
--- a/internal/sample_api/oas_response_encoders_gen.go
+++ b/internal/sample_api/oas_response_encoders_gen.go
@@ -496,13 +496,13 @@ func encodeTestMultipartResponse(response TestMultipartOK, w http.ResponseWriter
 	return nil
 
 }
-func encodeTestMultipartUploadResponse(response string, w http.ResponseWriter, span trace.Span) error {
+func encodeTestMultipartUploadResponse(response TestMultipartUploadOK, w http.ResponseWriter, span trace.Span) error {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(200)
 	span.SetStatus(codes.Ok, http.StatusText(200))
 	e := jx.GetEncoder()
 
-	e.Str(response)
+	response.Encode(e)
 	if _, err := e.WriteTo(w); err != nil {
 		return errors.Wrap(err, "write")
 	}
diff --git a/internal/sample_api/oas_schemas_gen.go b/internal/sample_api/oas_schemas_gen.go
index 9472c5bf..a264bb77 100644
--- a/internal/sample_api/oas_schemas_gen.go
+++ b/internal/sample_api/oas_schemas_gen.go
@@ -1898,6 +1898,52 @@ func (o OptMaxPropertiesTest) Or(d MaxPropertiesTest) MaxPropertiesTest {
 	return d
 }
 
+// NewOptMultipartFile returns new OptMultipartFile with value set to v.
+func NewOptMultipartFile(v ht.MultipartFile) OptMultipartFile {
+	return OptMultipartFile{
+		Value: v,
+		Set:   true,
+	}
+}
+
+// OptMultipartFile is optional ht.MultipartFile.
+type OptMultipartFile struct {
+	Value ht.MultipartFile
+	Set   bool
+}
+
+// IsSet returns true if OptMultipartFile was set.
+func (o OptMultipartFile) IsSet() bool { return o.Set }
+
+// Reset unsets value.
+func (o *OptMultipartFile) Reset() {
+	var v ht.MultipartFile
+	o.Value = v
+	o.Set = false
+}
+
+// SetTo sets value to v.
+func (o *OptMultipartFile) SetTo(v ht.MultipartFile) {
+	o.Set = true
+	o.Value = v
+}
+
+// Get returns value and boolean that denotes whether value was set.
+func (o OptMultipartFile) Get() (v ht.MultipartFile, ok bool) {
+	if !o.Set {
+		return v, false
+	}
+	return o.Value, true
+}
+
+// Or returns value if set, or given parameter if does not.
+func (o OptMultipartFile) Or(d ht.MultipartFile) ht.MultipartFile {
+	if v, ok := o.Get(); ok {
+		return v
+	}
+	return d
+}
+
 // NewOptNilString returns new OptNilString with value set to v.
 func NewOptNilString(v string) OptNilString {
 	return OptNilString{
@@ -3211,10 +3257,18 @@ type TestFormURLEncodedOK struct{}
 // TestMultipartOK is response for TestMultipart operation.
 type TestMultipartOK struct{}
 
+type TestMultipartUploadOK struct {
+	File         string    "json:\"file\""
+	OptionalFile OptString "json:\"optional_file\""
+	Files        []string  "json:\"files\""
+}
+
 type TestMultipartUploadReq struct {
-	OrderId  OptInt           "json:\"orderId\""
-	UserId   OptInt           "json:\"userId\""
-	FileName ht.MultipartFile "json:\"fileName\""
+	OrderId      OptInt             "json:\"orderId\""
+	UserId       OptInt             "json:\"userId\""
+	File         ht.MultipartFile   "json:\"file\""
+	OptionalFile OptMultipartFile   "json:\"optional_file\""
+	Files        []ht.MultipartFile "json:\"files\""
 }
 
 type TestObjectQueryParameterDeepObject struct {
diff --git a/internal/sample_api/oas_server_gen.go b/internal/sample_api/oas_server_gen.go
index 1262ad90..150459c8 100644
--- a/internal/sample_api/oas_server_gen.go
+++ b/internal/sample_api/oas_server_gen.go
@@ -157,7 +157,7 @@ type Handler interface {
 	// TestMultipartUpload implements testMultipartUpload operation.
 	//
 	// POST /testMultipartUpload
-	TestMultipartUpload(ctx context.Context, req TestMultipartUploadReq) (string, error)
+	TestMultipartUpload(ctx context.Context, req TestMultipartUploadReq) (TestMultipartUploadOK, error)
 	// TestObjectQueryParameter implements testObjectQueryParameter operation.
 	//
 	// GET /testObjectQueryParameter
diff --git a/internal/sample_api/oas_unimplemented_gen.go b/internal/sample_api/oas_unimplemented_gen.go
index 7bae32e0..9a13f9c5 100644
--- a/internal/sample_api/oas_unimplemented_gen.go
+++ b/internal/sample_api/oas_unimplemented_gen.go
@@ -245,7 +245,7 @@ func (UnimplementedHandler) TestMultipart(ctx context.Context, req TestForm) (r
 // TestMultipartUpload implements testMultipartUpload operation.
 //
 // POST /testMultipartUpload
-func (UnimplementedHandler) TestMultipartUpload(ctx context.Context, req TestMultipartUploadReq) (r string, _ error) {
+func (UnimplementedHandler) TestMultipartUpload(ctx context.Context, req TestMultipartUploadReq) (r TestMultipartUploadOK, _ error) {
 	return r, ht.ErrNotImplemented
 }
 
diff --git a/internal/sample_api/oas_validators_gen.go b/internal/sample_api/oas_validators_gen.go
index 5500b879..d8874ccf 100644
--- a/internal/sample_api/oas_validators_gen.go
+++ b/internal/sample_api/oas_validators_gen.go
@@ -954,6 +954,55 @@ func (s TestFloatValidation) Validate() error {
 	}
 	return nil
 }
+func (s TestMultipartUploadOK) Validate() error {
+	var failures []validate.FieldError
+	if err := func() error {
+		if s.Files == nil {
+			return errors.New("nil is invalid value")
+		}
+		if err := (validate.Array{
+			MinLength:    0,
+			MinLengthSet: false,
+			MaxLength:    5,
+			MaxLengthSet: true,
+		}).ValidateLength(len(s.Files)); err != nil {
+			return errors.Wrap(err, "array")
+		}
+		return nil
+	}(); err != nil {
+		failures = append(failures, validate.FieldError{
+			Name:  "files",
+			Error: err,
+		})
+	}
+	if len(failures) > 0 {
+		return &validate.Error{Fields: failures}
+	}
+	return nil
+}
+func (s TestMultipartUploadReq) Validate() error {
+	var failures []validate.FieldError
+	if err := func() error {
+		if err := (validate.Array{
+			MinLength:    0,
+			MinLengthSet: false,
+			MaxLength:    5,
+			MaxLengthSet: true,
+		}).ValidateLength(len(s.Files)); err != nil {
+			return errors.Wrap(err, "array")
+		}
+		return nil
+	}(); err != nil {
+		failures = append(failures, validate.FieldError{
+			Name:  "files",
+			Error: err,
+		})
+	}
+	if len(failures) > 0 {
+		return &validate.Error{Fields: failures}
+	}
+	return nil
+}
 func (s ValidationStringMap) Validate() error {
 	var failures []validate.FieldError
 	for key, elem := range s {


Test patch:
diff --git a/_testdata/positive/sample.json b/_testdata/positive/sample.json
index 5fd8c6c1..2b611c86 100644
--- a/_testdata/positive/sample.json
+++ b/_testdata/positive/sample.json
@@ -679,7 +679,8 @@
               "schema": {
                 "type": "object",
                 "required": [
-                    "fileName"
+                  "file",
+                  "files"
                 ],
                 "properties": {
                   "orderId": {
@@ -688,9 +689,21 @@
                   "userId": {
                     "type": "integer"
                   },
-                  "fileName": {
+                  "file": {
                     "type": "string",
                     "format": "binary"
+                  },
+                  "optional_file": {
+                    "type": "string",
+                    "format": "binary"
+                  },
+                  "files": {
+                    "type": "array",
+                    "items": {
+                      "type": "string",
+                      "format": "binary"
+                    },
+                    "maxItems": 5
                   }
                 }
               }
@@ -700,10 +713,32 @@
         "responses": {
           "200": {
             "description": "Ok",
-            "content":{
-              "application/json":{
+            "content": {
+              "application/json": {
                 "schema": {
-                  "type":"string"
+                  "type": "object",
+                  "required": [
+                    "file", 
+                    "files"
+                  ],
+                  "properties": {
+                    "file": {
+                      "type": "string",
+                      "format": "binary"
+                    },
+                    "optional_file": {
+                      "type": "string",
+                      "format": "binary"
+                    },
+                    "files": {
+                      "type": "array",
+                      "items": {
+                        "type": "string",
+                        "format": "binary"
+                      },
+                      "maxItems": 5
+                    }
+                  }
                 }
               }
             }
@@ -1964,4 +1999,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/form_test.go b/form_test.go
index f4ed977b..17f617d4 100644
--- a/form_test.go
+++ b/form_test.go
@@ -2,6 +2,7 @@ package ogen
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
@@ -11,6 +12,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/go-faster/errors"
 	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -63,15 +65,47 @@ func (s testFormServer) TestMultipart(ctx context.Context, req api.TestForm) (r
 	return r, nil
 }
 
-func (s testFormServer) TestMultipartUpload(ctx context.Context, req api.TestMultipartUploadReq) (r string, _ error) {
-	f := req.FileName
+func (s testFormServer) TestMultipartUpload(ctx context.Context, req api.TestMultipartUploadReq) (
+	r api.TestMultipartUploadOK,
+	_ error,
+) {
+	readFile := func(f ht.MultipartFile, to *string) error {
+		var b strings.Builder
+		if _, err := io.Copy(&b, f.File); err != nil {
+			return err
+		}
+		*to = b.String()
+		return nil
+	}
+
+	f := req.File
 	if val := f.Header.Get("Content-Type"); val != "image/jpeg" {
-		return "", validate.InvalidContentType(val)
+		return r, validate.InvalidContentType(val)
+	}
+
+	if err := readFile(req.File, &r.File); err != nil {
+		return r, errors.Wrap(err, "file")
+	}
+	if file, ok := req.OptionalFile.Get(); ok {
+		r.OptionalFile.Set = true
+		if err := readFile(file, &r.OptionalFile.Value); err != nil {
+			return r, errors.Wrap(err, "optional_file")
+		}
+	}
+	if err := func() error {
+		for idx, file := range req.Files {
+			var val string
+			if err := readFile(file, &val); err != nil {
+				return errors.Wrapf(err, "[%d]", idx)
+			}
+			r.Files = append(r.Files, val)
+		}
+		return nil
+	}(); err != nil {
+		return r, errors.Wrap(err, "files")
 	}
 
-	var b strings.Builder
-	_, err := io.Copy(&b, f.File)
-	return b.String(), err
+	return r, nil
 }
 
 func TestURIEncodingE2E(t *testing.T) {
@@ -145,16 +179,56 @@ func TestMultipartUploadE2E(t *testing.T) {
 	client, err := api.NewClient(s.URL, handler)
 	require.NoError(t, err)
 
-	data := "data"
-	r, err := client.TestMultipartUpload(ctx, api.TestMultipartUploadReq{
-		FileName: ht.MultipartFile{
-			Name: "pablo.jpg",
-			File: strings.NewReader(data),
-			Header: textproto.MIMEHeader{
-				"Content-Type": []string{"image/jpeg"},
-			},
-		},
-	})
-	a.NoError(err)
-	a.Equal(data, r)
+	tests := []struct {
+		name     string
+		file     string
+		optional api.OptString
+		array    []string
+		wantErr  bool
+	}{
+		{name: "OnlyFile", file: "data"},
+		{name: "All", file: "file", optional: api.NewOptString("optional"), array: []string{"1", "2"}},
+		{name: "TooBigArray", file: "file", array: []string{"1", "2", "3", "4", "5", "6"}, wantErr: true},
+	}
+	for _, tt := range tests {
+		if tt.array == nil {
+			tt.array = []string{}
+		}
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			a := require.New(t)
+
+			req := api.TestMultipartUploadReq{
+				File: ht.MultipartFile{
+					Name: "pablo.jpg",
+					File: strings.NewReader(tt.file),
+					Header: textproto.MIMEHeader{
+						"Content-Type": []string{"image/jpeg"},
+					},
+				},
+			}
+			if val, ok := tt.optional.Get(); ok {
+				req.OptionalFile = api.NewOptMultipartFile(ht.MultipartFile{
+					Name: "henry.jpg",
+					File: strings.NewReader(val),
+				})
+			}
+			for idx, val := range tt.array {
+				req.Files = append(req.Files, ht.MultipartFile{
+					Name: fmt.Sprintf("file%d.png", idx),
+					File: strings.NewReader(val),
+				})
+			}
+
+			r, err := client.TestMultipartUpload(ctx, req)
+			if tt.wantErr {
+				a.Error(err)
+				return
+			}
+			a.NoError(err)
+			a.Equal(tt.file, r.File)
+			a.Equal(tt.optional, r.OptionalFile)
+			a.Equal(tt.array, r.Files)
+		})
+	}
 }
diff --git a/gen_test.go b/gen_test.go
index 42916162..a8ed366a 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -123,7 +123,6 @@ func TestGenerate(t *testing.T) {
 		"superset.json": {
 			"allOf",
 			"unsupported content types",
-			"optional multipart file",
 		},
 	}
 
diff --git a/internal/sample_api/oas_test_examples_gen_test.go b/internal/sample_api/oas_test_examples_gen_test.go
index d28637c8..6ab095b7 100644
--- a/internal/sample_api/oas_test_examples_gen_test.go
+++ b/internal/sample_api/oas_test_examples_gen_test.go
@@ -727,6 +727,18 @@ func TestTestFloatValidation_EncodeDecode(t *testing.T) {
 	var typ2 TestFloatValidation
 	require.NoError(t, typ2.Decode(jx.DecodeBytes(data)))
 }
+func TestTestMultipartUploadOK_EncodeDecode(t *testing.T) {
+	var typ TestMultipartUploadOK
+	typ.SetFake()
+
+	e := jx.Encoder{}
+	typ.Encode(&e)
+	data := e.Bytes()
+	require.True(t, std.Valid(data), "Encoded: %s", data)
+
+	var typ2 TestMultipartUploadOK
+	require.NoError(t, typ2.Decode(jx.DecodeBytes(data)))
+}
 func TestTestObjectQueryParameterOK_EncodeDecode(t *testing.T) {
 	var typ TestObjectQueryParameterOK
 	typ.SetFake()