I have followed the README - specifically, this example - to create an OpenAPI definition and Swagger UI for my simple Flask-with-sqlalchemy app:
# main.py
#...
@app.route("/player", methods=["POST"])
def create_player():
"""Create a Player
---
requestBody:
description: Payload describing the player
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
example: Jim Bloggs
required:
- name
responses:
201:
description: Payload containing Player Id
schema:
type: object
properties:
id:
type: number
required:
- id
tags:
- player
"""
player = Player(name=data[request.json["name"]])
db.session.add(player)
db.session.commit()
return {"id": player.id}, 201
@app.route("/player/<player_id>")
def get_player(player_id: str):
"""Get a Player
---
parameters:
- name: player_id
in: path
required: true
schema:
type: integer
minimum: 1
description: The Player Id
responses:
200:
description: Payload describing player
content:
application/json:
schema:
type: object
properties:
id:
type: number
required: true
name:
type: string
required: true
404:
description: Player not found
content:
application/json: {}
tags:
- player
"""
player_from_db = db.session.get(Player, int(player_id))
if not player_from_db:
return "Not Found", 404
return _jsonify_and_remove_sqlalchemy_property(player_from_db)
# Note to StackOverflow readers - if you know a better way to do this, let me know!
def _jsonify_and_remove_sqlalchemy_property(o):
return {k: v for (k, v) in o.__dict__.items() if k != "_sa_instance_state"}
#...
# models.py
# where `db` is an instance of `SQLAlchemy()`
class Player(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
This works, but the definition of a Player is manually copied in three locations. If I added more API paths that used the same object, I'd have to copy it again - and if the definition changed, I'd have to update it manually everywhere. Not great.
I can imagine a few decreasingly-satisfying solutions to this issue, none of which I've been able to figure out how to implement:
Ideal solution - use class name directly in docstrings
Ideally, I'd like to be able to directly use the SQLAlchemy class' definition in the Swagger definitions - something like this (pseudo-syntax):
#...
@app.route("/player", methods=["POST"])
def create_player():
"""Create a Player
---
requestBody:
description: Payload describing the player
required: true
content:
application/json:
schema:
type: object
derivedFrom: Player.class
...
"""
#...
@app.route("/player/<player_id>")
def get_player(player_id: str):
"""Get a Player
---
parameters:
- name: player_id
in: path
required: true
schema:
type: integer
minimum: 1
description: The Player Id
responses:
200:
description: Payload describing player
content:
application/json:
schema:
type: object
derivedFrom: Player.class
...
"""
#...
In this hypothetical perfect situation, Flask would figure out that the derivedFrom: Player.class in the requestBody of create_player would not contain the id which is the PrimaryKey, but that the derivedFrom: Player.class in the responses of get_player would.
This is probably not possible in the vaguely-magical way I've described here, since Swagger itself probably needs to be informed of the classes to use when hydrating the specs - but I figured I'd describe it as the ideal approach in case there is a possibility that is somewhat closer to this than anything else I describe.
Less-good-but-still-good solution - specify external definitions from a class
OpenAPI has the notion of Components, which are externalized fragments of various types (including schemas) that can be referenced in a spec. Something like this would be great:
app = Flask(__name__)
swagger = Swagger(app)
# This next line is hypothetical
swagger.addSchemaFromClass(Player.class)
#...
@app.route("/player", methods=["POST"])
def create_player():
"""Create a Player
---
requestBody:
description: Payload describing the player
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Player"
...
"""
#...
Again, in an ideal world Swagger would be smart enough to know that a POST method that takes an object in the body should be lacking the id, whereas a GET method that returns it should have the id - but it's not the end of the world if that's not possible, the returning-cases should be far more numerous than the creation cases.
I did find Swagger.definition, which seems to be an annotation that can be used to do something like this, but it's not ideal - it appears to still require docstrings to define the schema, rather than reading them from the class directly, at which point this becomes basically equivalent to the next option.
From here it appears that direct class-based definitions can be achieved using another library named Marshmellow, which I'll explore if there are no better options using just the libraries I have (Flask-SQLAlchemy & flasgger (though I suspect it still won't do the intelligent "with/without ids" behaviour that I've described.
Still acceptable solution - specify external definition directly
If there's a way of directly adding a component to swagger, I'm sure I could write a function to convert from a SQLAlchemy class to a definition, and do something like:
app = Flask(__name__)
swagger = Swagger(app)
# This next line is hypothetical
swagger.addSchema(convertFromSqlAlchemyClassToSchema(Player.class))
# And, hey, if I'm going manual, I may as well also directly implement the with/without id logic I've referred to
swagger.addSchema(convertFromSqlAlchemyClassToSchemaWithoutPrimaryKey(Player.class))
#...
@app.route("/player", methods=["POST"])
def create_player():
"""Create a Player
---
requestBody:
description: Payload describing the player
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Player_WithoutPrimaryKey"
...
"""
#...
Failing all the previous solutions - is this possible? I see from here that it is possible to specify components directly at init-time of the Swagger object - but then it looks like I'd have to specify my whole Swagger config directly, rather than using the docstring-based approach I'm already using, and I'd be loath to give up the colocation of the definition with the methods.