python-restx / flask-restx

Fork of Flask-RESTPlus: Fully featured framework for fast, easy and documented API development with Flask
https://flask-restx.readthedocs.io/en/latest/
Other
2.16k stars 335 forks source link

Unable to use models created with @api.schema_model with @api.expect if they have nested objects #494

Closed poopledc closed 1 year ago

poopledc commented 1 year ago

As the title says, unable to use json schemas to properly render the swagger documentation. I am using the example from the link found here: https://flask-restx.readthedocs.io/en/latest/marshalling.html#define-model-using-json-schema to load in a JSON schema with a nested reference. Then I use the @api.expect decorator to add the Person object to the GET method. I load up the SwaggerUI at http://localhost:5000 and there are Swagger errors present

Code

from flask import Flask
from flask_restx import Api, Resource

app = Flask(__name__)
api = Api(app)

address = api.schema_model('Address', {
    'properties': {
        'road': {
            'type': 'string'
        },
    },
    'type': 'object'
})

person = api.schema_model('Person', {
    'required': ['address'],
    'properties': {
        'name': {
            'type': 'string'
        },
        'age': {
            'type': 'integer'
        },
        'birthdate': {
            'type': 'string',
            'format': 'date-time'
        },
        'address': {
            '$ref': '#/definitions/Address',
        }
    },
    'type': 'object'
})

@api.route("/")
class TestResource(Resource):
    @api.expect(person)
    def get(self):
        return {"hello": "world"}

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

Here is the Swagger Doc produced: image

Here is the swagger.json produced

{
    "swagger": "2.0",
    "basePath": "\/",
    "paths": {
        "\/": {
            "get": {
                "responses": {
                    "200": {
                        "description": "Success"
                    }
                },
                "operationId": "get_test_resource",
                "parameters": [
                    {
                        "name": "payload",
                        "required": true,
                        "in": "body",
                        "schema": {
                            "$ref": "#\/definitions\/Person"
                        }
                    }
                ],
                "tags": [
                    "default"
                ]
            }
        }
    },
    "info": {
        "title": "API",
        "version": "1.0"
    },
    "produces": [
        "application\/json"
    ],
    "consumes": [
        "application\/json"
    ],
    "tags": [
        {
            "name": "default",
            "description": "Default namespace"
        }
    ],
    "definitions": {
        "Person": {
            "required": [
                "address"
            ],
            "properties": {
                "name": {
                    "type": "string"
                },
                "age": {
                    "type": "integer"
                },
                "birthdate": {
                    "type": "string",
                    "format": "date-time"
                },
                "address": {
                    "$ref": "#\/definitions\/Address"
                }
            },
            "type": "object"
        }
    },
    "responses": {
        "ParseError": {
            "description": "When a mask can't be parsed"
        },
        "MaskError": {
            "description": "When any error occurs on mask"
        }
    }
}

Repro Steps (if applicable)

  1. Create basic Flask app using flask and flask-restx
  2. Create models using api.schema_model
  3. Create a test endpoint using @api.route and Resource
  4. Create a GET method
  5. Add @api.expect(schema_model)
  6. Run Flask app
  7. Open web browser and navigate to http://localhost:5000 to get to SwaggerUI
  8. Expand Swagger definitions and see errors

Expected Behavior

No errors and the address field should populate properly under the payload section

Actual Behavior

Errors are thrown and the address field is listed as as string

Error Messages/Stack Trace

Resolver error at definitions.Person.properties.address.$ref Could not resolve reference: Could not resolve pointer: /definitions/Address does not exist in document Resolver error at paths./.get.parameters.0.schema.properties.address.$ref Could not resolve reference: Could not resolve pointer: /definitions/Address does not exist in document

Environment

poopledc commented 1 year ago

OK so I tried out the same example using api.model and it worked. Code below

import restx_monkey as monkey

monkey.patch_restx()
from flask import Flask
from flask_restx import Api, Resource, fields

app = Flask(__name__)
api = Api(app)

address = api.model("Address", {"road": fields.String(required=False)})

person = api.model(
    "Person",
    {
        "address": fields.Nested(address, required=True),
        "name": fields.String(required=False),
        "age": fields.Integer(required=False),
        "birthdate": fields.DateTime(required=False),
    },
)

@api.route("/")
class TestResource(Resource):
    @api.expect(person)
    def get(self):
        return {"hello": "world"}

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

And here's the Swagger page: image

And here's there swagger.json

{
    "swagger": "2.0",
    "basePath": "\/",
    "paths": {
        "\/": {
            "get": {
                "responses": {
                    "200": {
                        "description": "Success"
                    }
                },
                "operationId": "get_test_resource",
                "parameters": [
                    {
                        "name": "payload",
                        "required": true,
                        "in": "body",
                        "schema": {
                            "$ref": "#\/definitions\/Person"
                        }
                    }
                ],
                "tags": [
                    "default"
                ]
            }
        }
    },
    "info": {
        "title": "API",
        "version": "1.0"
    },
    "produces": [
        "application\/json"
    ],
    "consumes": [
        "application\/json"
    ],
    "tags": [
        {
            "name": "default",
            "description": "Default namespace"
        }
    ],
    "definitions": {
        "Person": {
            "required": [
                "address"
            ],
            "properties": {
                "name": {
                    "type": "string"
                },
                "age": {
                    "type": "integer"
                },
                "birthdate": {
                    "type": "string",
                    "format": "date-time"
                },
                "address": {
                    "$ref": "#\/definitions\/Address"
                }
            },
            "type": "object"
        },
        "Address": {
            "properties": {
                "road": {
                    "type": "string"
                }
            },
            "type": "object"
        }
    },
    "responses": {
        "ParseError": {
            "description": "When a mask can't be parsed"
        },
        "MaskError": {
            "description": "When any error occurs on mask"
        }
    }
}

So it looks like the Address model isn't added to the "definitions" block using the api.schema_model call, whereas the api.model call adds it.

poopledc commented 1 year ago

OK I think this is the solution: https://github.com/python-restx/flask-restx/issues/59#issuecomment-899790061

app["RESTX_INCLUDE_ALL_MODELS"] = True must be added

import restx_monkey as monkey
monkey.patch_restx()
from flask import Flask
from flask_restx import Api, Resource

app = Flask(__name__)
api = Api(app)
app.config["RESTX_INCLUDE_ALL_MODELS"] = True

address = api.schema_model('Address', {
    'properties': {
        'road': {
            'type': 'string'
        },
    },
    'type': 'object'
})

person = api.schema_model('Person', {
    'required': ['address'],
    'properties': {
        'name': {
            'type': 'string'
        },
        'age': {
            'type': 'integer'
        },
        'birthdate': {
            'type': 'string',
            'format': 'date-time'
        },
        'address': {
            '$ref': '#/definitions/Address',
        }
    },
    'type': 'object'
})

@api.route("/")
class TestResource(Resource):
    @api.expect(person)
    def get(self):
        return {"hello": "world"}

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