How to use an existing flask-sqlalchemy class as a definition in flask-swagger

50 Views Asked by At

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.

0

There are 0 best solutions below