spectree icon indicating copy to clipboard operation
spectree copied to clipboard

[BUG] There is no way to create swagger file upload

Open kemara opened this issue 4 years ago • 10 comments

Describe the bug As json takes a pynatic model and there is no Pyndatic field with type file I could not find a way to upload a file.

Does anybody know of a way to create a swagger file upload in Flask?

kemara avatar Nov 23 '20 10:11 kemara

Thanks for your feedback.

Pydantic doesn't have a file type. Actually, flask will create a werkzeug.datastructures.FileStorage object. To support this, you need to provide a File type.

from werkzeug.datastructures import FileStorage
from pydantic import BaseModel


class File:
    file: FileStorage
    class Config:
        arbitrary_types_allowed = True

Let's say if you have multiple items in the form, then you may need to create the schema like this:

from werkzeug.datastructures import FileStorage
from pydantic import BaseModel


class Form:
    file: FileStorage
    user: str
    addr: str

    class Config:
        arbitrary_types_allowed = True

I'll create a PR to support this.

kemingy avatar Nov 23 '20 14:11 kemingy

Pydantic doesn't have a file type. Actually, flask will create a werkzeug.datastructures.FileStorage object. To support this, you need to provide a File type.

The problem is that cannot generate the schema because FileStorage is not a pydantic model.

I guess we need to create another model like this:

class FlaskFile(BaseModel):
    filename: str
    name: str
    content_length: int
    content_type: str
    mimetype: str
    stream: bytes

kemingy avatar Nov 24 '20 04:11 kemingy

Thank you for your help but I was still unable to create a functioning swagger doc that can upload a file.

When I try to add it to a spectree.validate as json or header an error accurs: Value not declarable with JSON Schema, field: name='file' type=FileStorage required=True

As I understood you we need to add FlaskFile into spectree so it can handle it. Is there any way I could help with that?

kemara avatar Nov 25 '20 07:11 kemara

I found a solution. It may not be elegant.

from werkzeug.datastructures import FileStorage
from pydantic import BaseModel


FlaskFile = FileStorage


def file_modify_schame(cls, field_schema):
    field_schema.update(format='file-storage')


FlaskFile.__modify_schema__ = classmethod(file_modify_schame)


class Form(BaseModel):
    user: str
    file: FlaskFile

    class Config:
        arbitrary_types_allowed = True

The code above will work with this commit: https://github.com/0b01001001/spectree/commit/53936d41463c0a7020dc2174b06b6b6187c3028d

Let me know if you have any ideas.

kemingy avatar Nov 25 '20 14:11 kemingy

Thank you again for your help but I was still not able to make it work. Here is the complete flask application where I tried to make it work:

from flask import Flask
from spectree import SpecTree
from werkzeug.datastructures import FileStorage
from pydantic import BaseModel

FlaskFile = FileStorage

def file_modify_schame(cls, field_schema):
    field_schema.update(format='file-storage')

FlaskFile.__modify_schema__ = classmethod(file_modify_schame)

class Form(BaseModel):
    user: str
    file: FlaskFile

    class Config:
        arbitrary_types_allowed = True

app = Flask(__name__)
api = SpecTree('flask')

@app.route('/', methods=['POST'])
@api.validate(json=Form)
def hello_world():
    return 'Hello World!'

api.register(app)
if __name__ == '__main__':
    app.run()

The swagger looks like: Screenshot 2020-12-09 at 10 57 54

Is there anything wrong that I am doing?

kemara avatar Dec 09 '20 09:12 kemara

Hi, I think your code is correct. But sadly swagger doesn't support the customized type of object. I think you can find the definition in swagger schemas below or redoc page (/apidoc/redoc).

kemingy avatar Dec 09 '20 14:12 kemingy

The problem is that spectree only parse request body for json on docs generation.

if hasattr(func, "json"):
        data = {"content": {"application/json": {"schema": {"$ref": f"#/components/schemas/{func.json}"}}}}

In the request context, spectree convert both request.form and request.json in the same request.context.json.

I'm start to use spectree in my work and I made some modifications to put @api.validate(json=JsonSchema) or @api.validate(form=FormSchema), using that, I modificate the request body parser for docs, to diferentiate the two types

if hasattr(func, "json"):
        data = {"content": {"application/json": {"schema": {"$ref": f"#/components/schemas/{func.json}"}}}}

if hasattr(func, "form"):
        data = {"content": {"multipart/form-data": {"schema": {"$ref": f"#/components/schemas/{func.form}"}}}}

After do that, you will be able to create a pydantic custom schema for file that has a customized schema that returns the openapi spec for file upload.

class UploadedFile:
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        return v
    @classmethod
    def __modify_schema__(cls, field_schema):
        field_schema["type"] = "file"
        field_schema["format"] = "binary"

gomeslucasm avatar May 19 '22 16:05 gomeslucasm

Are there some reasons for choosing to add both request.form or request.json in the same @api.validate(json=Schema)? Instead of declaring the two different schemas?

gomeslucasm avatar May 19 '22 16:05 gomeslucasm

Are there some reasons for choosing to add both request.form or request.json in the same @api.validate(json=Schema)? Instead of declaring the two different schemas?

The request body can be either JSON data (maybe other serialized type) or a file.

There is a PR related to this:

  • #185

This PR doesn't have activities for a long time. Hope we can work together to improve it.

kemingy avatar May 19 '22 16:05 kemingy

Interesting, maybe I can help you, I don't have knowledge about starlette or falcon, but I can give you some ideas about the modifications that I made in the flask plugin.

gomeslucasm avatar May 19 '22 16:05 gomeslucasm

closing, solved via #225

yedpodtrzitko avatar Mar 22 '23 19:03 yedpodtrzitko