flask-restx
flask-restx copied to clipboard
Not rendering swagger schema when multifile param and body is present
Hi,
I am developing an API with a POST method endpoint accepting:
- Authorization: Bearer
- multiple file list
- json body
Params are defined in the following way:
def authorization_param(ns: Namespace, parser: Optional[RequestParser] = None) -> RequestParser:
if not parser:
parser = ns.parser()
parser.add_argument('Authorization', location='headers', required=False, default='Bearer ')
return parser
def multiple_file_param(arg_name: str, ns: Namespace, parser: Optional[RequestParser] = None) -> RequestParser:
if not parser:
parser = ns.parser()
parser.add_argument(arg_name, type=FileStorage, location='files', required=True, action='append')
return parser
Model:
some_form_model = api.model('form', {'field': fields.String())
In the endpoint module:
ns = Namespace('sth', description='Some stuff'))
auth_param = authorization_param(ns=ns)
file_param = multiple_file_param(arg_name='File', ns=ns)
@ns.route('/files')
class PreprocessFiles(Resource):
@ns.expect(auth_param, file_param, some_form_model)
def post(self):
payload = request.get_json()
# do some stuff..
return {'text': 'ok'}, 201
Error Messages/Stack Trace
2022-08-27 13:06:46.642 ERROR flask_restx.api api.__schema__: Unable to render schema
Traceback (most recent call last):
File "D:\project\venv\lib\site-packages\flask_restx\api.py", line 571, in __schema__
self._schema = Swagger(self).as_dict()
File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 239, in as_dict
serialized = self.serialize_resource(
File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 446, in serialize_resource
path[method] = self.serialize_operation(doc, method)
File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 469, in serialize_operation
if any(p["type"] == "file" for p in all_params):
File "D:\project\venv\lib\site-packages\flask_restx\swagger.py", line 469, in <genexpr>
if any(p["type"] == "file" for p in all_params):
KeyError: 'type'
The error is raised by the following method from flask_restx.swagger
:
def serialize_operation(self, doc, method):
operation = {
"responses": self.responses_for(doc, method) or None,
"summary": doc[method]["docstring"]["summary"],
"description": self.description_for(doc, method) or None,
"operationId": self.operation_id_for(doc, method),
"parameters": self.parameters_for(doc[method]) or None,
"security": self.security_for(doc, method),
}
# Handle 'produces' mimetypes documentation
if "produces" in doc[method]:
operation["produces"] = doc[method]["produces"]
# Handle deprecated annotation
if doc.get("deprecated") or doc[method].get("deprecated"):
operation["deprecated"] = True
# Handle form exceptions:
doc_params = list(doc.get("params", {}).values())
all_params = doc_params + (operation["parameters"] or [])
if all_params and any(p["in"] == "formData" for p in all_params):
if any(p["type"] == "file" for p in all_params):
operation["consumes"] = ["multipart/form-data"]
else:
operation["consumes"] = [
"application/x-www-form-urlencoded",
"multipart/form-data",
]
operation.update(self.vendor_fields(doc, method))
return not_none(operation)
specifically:
if all_params and any(p["in"] == "formData" for p in all_params):
if any(p["type"] == "file" for p in all_params):
operation["consumes"] = ["multipart/form-data"]
else:
operation["consumes"] = [
"application/x-www-form-urlencoded",
"multipart/form-data",
]
It is raised since body parameter does not have type
key. If I list here all_param variable it will be:
[
{
"name": "File",
"in": "formData",
"type": "array",
"required": true,
"items": {
"type": "file"
},
"collectionFormat": "multi"
},
{
"name": "Authorization",
"in": "header",
"type": "string",
"default": "Bearer "
},
{
"name": "payload",
"required": true,
"in": "body",
"schema": {
"$ref": "#/definitions/PipelineConfig"
}
}
]
Same error happens if I change the way I decorate endpoint class and method:
@ns.route('/files')
@ns.expect(auth_param, file_param)
class PreprocessFiles(Resource):
@ns.expect(some_form_model)
def post(self):
payload = request.get_json()
# do some stuff..
return {'text': 'ok'}, 201
As I have started testing following modification to the 'faulty' code shall be enough as it allows to render schema and have documentation with all required parameters.
all_params = doc_params + (operation["parameters"] or [])
if all_params and any(p["in"] == "formData" for p in all_params):
if any(p.get("type", None) == "file" for p in all_params):
operation["consumes"] = ["multipart/form-data"]
else:
operation["consumes"] = [
"application/x-www-form-urlencoded",
"multipart/form-data",
]
I will happily make a pull request if such a solution is good enough. However, perhaps this issue can be handled in another way.
Environment
- Python version - 3.9
- Flask version - 2.1.3
- Flask-RESTX version - 0.5.1
I think the issue is that api.expect()
and namespace.expect()
expect you to pass a single expected model (or a list of the same expected model) or request parser? I can't exactly recreate the issue on my side for some reason.
My suggestion would be to document the authorizations separately, as described here: https://flask-restx.readthedocs.io/en/latest/swagger.html#documenting-authorizations
Then you can also build models that are composed of other models using fields.nested()
.