Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error loading Swagger docs when transforming a flat object to a nested structure #756

Open
jlongo-encora opened this issue Dec 3, 2019 · 10 comments
Labels

Comments

@jlongo-encora
Copy link

jlongo-encora commented Dec 3, 2019

Description

I could marshall a flat object to a nested structure using the example from https://flask-restplus.readthedocs.io/en/stable/marshalling.html#complex-structures :

{
  "id": 1,
  "phone": 123456
}

into

{
  "id": 1,
  "phone": {
    "cell": 123456
  }
}

However, the Swagger docs stop working.

Code

app.py

from flask import Flask
from flask_restplus import Api, Resource, fields
from werkzeug.contrib.fixers import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
api = Api(app, version="1.0", title="User API", description="A simple User API",)

ns = api.namespace('users', description='User operations')

phone = api.model("phone", {"cell": fields.Integer(required=False)})
user = api.model("user", {"id": fields.Integer(required=True)})
# From https://flask-restplus.readthedocs.io/en/stable/marshalling.html#complex-structures
user["phone"] = phone

@ns.route("/")
class GetUser(Resource):
    @ns.marshal_with(user)
    def get(self):
        return {"id": 1, "phone": 123456}

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

requirements.txt (from pip install Flask and pip install flask-restplus)

aniso8601==8.0.0
attrs==19.3.0
Click==7.0
Flask==1.1.1
flask-restplus==0.13.0
importlib-metadata==1.1.0
itsdangerous==1.1.0
Jinja2==2.10.3
jsonschema==3.2.0
MarkupSafe==1.1.1
more-itertools==8.0.0
pyrsistent==0.15.6
pytz==2019.3
six==1.13.0
Werkzeug==0.16.0
zipp==0.6.0

Repro Steps (if applicable)

  1. Run "pip install -r requirements.txt"
  2. Run the code above as "python app.py"
  3. Enter on http://localhost:5000

Expected Behavior

Swagger UI should render correctly

Actual Behavior

You'll see the message: "No API definition provided"

Error Messages/Stack Trace

10.0.2.2 - - [03/Dec/2019 09:59:46] "GET / HTTP/1.1" 200 -
Unable to render schema
Traceback (most recent call last):
  File "/home/jlongo/.virtualenvs/example/lib/python3.7/site-packages/flask_restplus/api.py", line 484, in __schema__
    self._schema = Swagger(self).as_dict()
  File "/home/jlongo/.virtualenvs/example/lib/python3.7/site-packages/flask_restplus/swagger.py", line 219, in as_dict
    'definitions': self.serialize_definitions() or None,
  File "/home/jlongo/.virtualenvs/example/lib/python3.7/site-packages/flask_restplus/swagger.py", line 542, in serialize_definitions
    for name, model in iteritems(self._registered_models)
  File "/home/jlongo/.virtualenvs/example/lib/python3.7/site-packages/flask_restplus/swagger.py", line 542, in <genexpr>
    for name, model in iteritems(self._registered_models)
  File "/home/jlongo/.virtualenvs/example/lib/python3.7/site-packages/flask_restplus/model.py", line 72, in __schema__
    schema = self._schema
  File "/home/jlongo/.virtualenvs/example/lib/python3.7/site-packages/flask_restplus/model.py", line 148, in _schema
    if field.required:
AttributeError: 'Model' object has no attribute 'required'
10.0.2.2 - - [03/Dec/2019 09:59:47] "GET /swagger.json HTTP/1.1" 500 -

Environment

  • Python version: 3.7.5
  • Flask version: 1.1.1
  • Flask-RESTPlus version: 0.13.0
  • Other installed Flask extensions: no
@kovalevfm
Copy link

I think you need to use nested fields

@jlongo-encora
Copy link
Author

jlongo-encora commented Dec 3, 2019

@kovalevfm

If I use nested fields, like:

phone = api.model("phone", {
    "cell": fields.Integer(required=False, attribute="phone")
})
user = api.model("user", {
    "id": fields.Integer(required=True),
    "phone": fields.Nested(phone)
})

The Swagger docs work, but the result is not correct:

{
  "id": 1,
  "phone": {
    "cell": null
  }
}

As far as I understood, nested fields would be to marshall an object with already a nested structure. Am I wrong?

@kovalevfm
Copy link

Would it work as supposed if you write following?

@ns.route("/")
class GetUser(Resource):
    @ns.marshal_with(user)
    def get(self):
        return {"id": 1, "phone": {"cell":123456}}

@jlongo-encora
Copy link
Author

jlongo-encora commented Dec 4, 2019

@kovalevfm
Yes, and that's why I opened this ticket. My object is a result of a raw SQL query and I'd need to marshall it into a nested structure. Actually it's working as described in the documentation (using complex structures), but the Swagger docs break. It looks like a bug

@mefyl
Copy link

mefyl commented Dec 20, 2019

Seconded, I just stumbled on this an came to the exact same conclusion.

@rhuart46
Copy link

rhuart46 commented Jun 11, 2020

The object you're trying to marshal is:
{"id": 1, "phone": 123456}

But the model you define when you write

phone = api.model("phone", {
    "cell": fields.Integer(required=False, attribute="phone")
})
user = api.model("user", {
    "id": fields.Integer(required=True),
    "phone": fields.Nested(phone)
})

expects an object of the form:
{"id": 1, "phone": {"cell":123456}}

Hence the doc is good but the object cannot be marshalled correctly, I think it's what @kovalevfm meant in his answer.

@jlongo-encora
Copy link
Author

jlongo-encora commented Jun 11, 2020

@Tulkas46
The doc (https://flask-restplus.readthedocs.io/en/stable/marshalling.html#complex-structures) says I could do:

phone = api.model("phone", {"cell": fields.Integer(required=False)})
user = api.model("user", {"id": fields.Integer(required=True)})
user["phone"] = phone

and it's marshalling correct. However, the swagger page breaks. That's the issue

@rhuart46
Copy link

rhuart46 commented Jun 11, 2020

Sorry but the link you provide shows uses of simple dicts of fields.[Type] used with the function marshal, not the api.models mix you did try used with the method Namespace.marshal_with. Yes, api.models are usable as dicts, but it does not mean that what you tried is supposed to work, imho.

I did elaborate on your example to show some possibilities to readers:

from flask import Flask
from flask_restplus import Api, Resource, fields, marshal
from werkzeug.contrib.fixers import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
api = Api(app, version="1.0", title="User API", description="A simple User API",)

ns = api.namespace('users', description='User operations')

# Object with structure A, response with structure A:
user_a = api.model("user_a", {"id": fields.Integer})
user_a["phone"] = fields.Integer

@ns.route("/A")
class GetUserA(Resource):
    @ns.marshal_with(user_a)
    def get(self):
        return {"id": 1, "phone": 123456}

# Object with structure B, response with structure B:
phone_b = api.model("phone_b", {"cell": fields.Integer})
user_b = api.model("user_b", {"id": fields.Integer})
user_b["phone"] = fields.Nested(phone_b)

@ns.route("/B")
class GetUserB(Resource):
    @ns.marshal_with(user_b)
    def get(self):
        return {"id": 1, "phone": {"cell": 123456}}

# Object with structure B, response with structure A:
user_c = api.model("user_c", {"id": fields.Integer})
user_c["phone"] = fields.Integer(attribute="phone.cell")

@ns.route("/C")
class GetUserC(Resource):
    @ns.marshal_with(user_c)
    def get(self):
        return {"id": 1, "phone": {"cell": 123456}}

# Object with structure A, response with structure B (needs to decorrelate documentation and marshalling):
user_d_fields = {"id": fields.Integer}
user_d_fields["phone"] = {}
user_d_fields["phone"]["cell"] = fields.Integer(attribute="phone")
phone_d = api.model("phone_d", {"cell": fields.Integer})
user_d = api.model("user_d", {"id": fields.Integer, "phone": fields.Nested(phone_d)})

@ns.route("/D")
class GetUserD(Resource):
    @ns.response(200, "Get object A in B format", user_d)
    @ns.marshal_with(user_d_fields)
    def get(self):
        return {"id": 1, "phone": 123456}

if __name__ == "__main__":
    # Test OP's original attempt with the marshalling alone, without documentation:
    phone = api.model("phone", {"cell": fields.Integer(required=False)})
    user = api.model("user", {"id": fields.Integer(required=True)})
    user["phone"] = phone

    print("res =", marshal({"id": 1, "phone": 123456}, user))
    # ^ Output: res = {'id': 1, 'phone': {'cell': None}}  # fails

    print("res =", marshal({"id": 1, "phone": {"cell": 123456}}, user))
    # ^ Output: res = {'id': 1, 'phone': {'cell': None}}  # fails

    app.run(host="0.0.0.0", debug=True)

@jlongo-daitan I'm afraid you have to use something like GetUserD which is a little bit more complex than what you tried.

@jlongo-encora
Copy link
Author

@Tulkas46 I fixed that issue by transforming the object before marshalling.

Anyway, the doc was making me think that would be possible

@Cnoor0171
Copy link

This definitely seems like a bug with the swagger docs generation. Yes its possible to work around it by decoupling the response marshalling and documentation generation, but that kind of defeats the purpose of using the marshal_with decorators. Especially surprising because the docs for models say that they can be used for response marshalling. Worth another look, I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants