wI2L / fizz

:lemon: Gin wrapper with OpenAPI 3 spec generation
https://pkg.go.dev/github.com/wI2L/fizz
MIT License
214 stars 52 forks source link

How to document multipart/form-data? #44

Closed nikicc closed 3 years ago

nikicc commented 3 years ago

I would like to generate the OpenAPI specification for the endpoint that receives the request body as multipart/form-data. Is this possible?

I'm pasting my attempts below, but all of them resulted in the request body to be documented as application/json.

Code:

package main

import (
    "fmt"
    "mime/multipart"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/loopfz/gadgeto/tonic"
    "github.com/wI2L/fizz"
)

type CreateForm struct {
    Message string                `form:"message"`
    File    *multipart.FileHeader `form:"file"`
}

// ATTEMPT 1: tried to bind with the f parameter: marks is as JSON
func handler(c *gin.Context, f *CreateForm) error {
    var form CreateForm
    if err := c.ShouldBindWith(&form, binding.FormMultipart); err != nil {
        return err
    }

    // more implementation here
    return nil
}

func main() {
    f := fizz.NewFromEngine(gin.New())
    f.GET("/openapi", nil, f.OpenAPI(nil, "yaml"))

    f.POST(
        "/create",
        []fizz.OperationOption{
            // ATTEMPT 2: try to bind with the fizz.InputModel: marks is as JSON
            fizz.InputModel(CreateForm{}),
        },
        tonic.Handler(
            handler,
            201,
        ),
    )

    fmt.Println("Errors:", f.Errors())

    srv := &http.Server{Addr: ":8090", Handler: f}
    srv.ListenAndServe()
}

Generated OpenAPI:

openapi: 3.0.1
info: null
paths:
  /create:
    post:
      operationId: handler
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                File:
                  $ref: '#/components/schemas/MultipartFileHeader'
                Message:
                  type: string
      responses:
        "201":
          description: Created
components:
  schemas:
    MultipartFileHeader:
      type: object
      properties:
        Filename:
          type: string
        Header:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
        Size:
          type: integer
          format: int64
nikicc commented 3 years ago

If this is not yet supported, I can try to implement it and make a PR. My first idea would be to extend the fizz.InputModel to accept another parameter to set the body type (e.g. multipart/form-data). Any thoughts about this?

wI2L commented 3 years ago

There's two part to what you want to achieve.

First, the binding of the body. It isn't directly related to this library, since it uses https://github.com/loopfz/gadgeto. See https://github.com/loopfz/gadgeto/blob/master/tonic/tonic.go#L99 for example, you can see the default JSON binding in the default hook. You need to change the bind hook, but be aware that it changes the hook globally, there's no per-handler support in tonic. If that's something you require, that would require a PR.

Second, Fizz (the openapi generator) infers the media type from tonic with tonic.MediaType(). Again, this is a global function and the returned value is assumed for all operations bodies. Changes would be necessary to have per-handler/route media types.

NOTE:

The first example you gave won't work because tonic has already binded everything to the f instance of the *CreateForm type.

func handler(c *gin.Context, f *CreateForm) error {
    var form CreateForm
    if err := c.ShouldBindWith(&form, binding.FormMultipart); err != nil {
        return err
    }

    // more implementation here
    return nil
}

Of course, by rebinding the gin.Context to a new instance form, that should works, but it defeats the concept of using tonic in the first place, and that would require to manually indicate what is the media type of the body in the router.

mcorbin commented 2 years ago

I started working on this issue which is interesting to solve because it's common to have APIs serving differents media types. I did https://github.com/loopfz/gadgeto/pull/87 on Tonic and I already did the changes to support it on Fizz: https://github.com/wI2L/fizz/commit/9fa8e0b3ca3e7f9f25b96ca3361bc586185be1d5. If my PR is merged on Tonic I will open another one here as well (if not, I will merge it on my fork).