Frameworks
Generate an OpenAPI/Swagger document with Flask
How to generate an OpenAPI document with Flask
When building REST APIs with Flask, clear and accurate documentation helps API consumers integrate quickly and confidently. The OpenAPI Specification has become the industry standard for documenting RESTful APIs, but writing and maintaining OpenAPI documents by hand is tedious and error-prone.
The flask-smorest extension solves this by generating OpenAPI documentation directly from Flask code, using decorators and marshmallow schemas already in use. This guide covers generating an OpenAPI document from a Flask project and using it to create SDKs with Speakeasy, covering the following:
- Setting up a Flask REST API with
flask-smorest - Integrating OpenAPI document generation
- Generating and customizing the OpenAPI spec
- Using the Speakeasy CLI to generate client SDKs
What is flask-smorest?
flask-smorest is a Flask extension that generates OpenAPI documentation from application code. Unlike tools that parse docstrings or require separate spec files, flask-smorest uses decorators on view classes to describe endpoints, validate request data, and serialize responses — all while automatically building an accurate OpenAPI document.
flask-smorest builds on two Flask concepts:
Blueprint: flask-smorest’sBlueprintextends Flask’s built-in blueprint with OpenAPI integration. Blueprints are registered with theApiobject instead of the Flask app directly.MethodView: Flask’s class-based views groupget,post,put, anddeletemethods under a single route, making it easy to apply flask-smorest decorators consistently.
The two main decorators are:
@blp.response(status_code, schema): Specifies the response code and marshmallow schema for a method. flask-smorest uses this to document the response and serialize the return value.@blp.arguments(schema): Validates and deserializes the request body against a marshmallow schema, and documents the expected request body in the OpenAPI spec.
Sample API code
Example repository
The source code for the completed example is available in the Speakeasy examples repository (framework-flask).
The repository contains all the code covered throughout this guide. Clone it to follow along, or use it as a reference for an existing Flask project.
For this guide, we’ll use a simple Formula 1 (F1) lap times API with the following resources:
- Circuits: Racing circuits with names and locations
- Drivers: F1 drivers with their names and codes
- Lap Times: Records of lap times for drivers on specific circuits
Requirements
To follow this guide:
- Python 3.8 or higher
- An existing Flask project or a copy of the provided example repository
- A basic understanding of Flask and REST APIs
Setting up flask-smorest
To follow along with the example repository, create and activate a virtual environment and install the project dependencies:
python -m venv venvsource venv/bin/activatepip install -r requirements.txtTo add flask-smorest to an existing project, install it with:
pip install flask-smorestConfiguring flask-smorest
The core configuration for generating an OpenAPI document is added in app.py:
from flask import Flaskfrom flask_smorest import Apifrom flask_migrate import Migratefrom db import dbimport modelsfrom resources import CircuitBlueprint, DriverBlueprint, LapTimeBlueprintimport yaml
app = Flask(__name__)app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = Falseapp.config["API_TITLE"] = "F1 Laps API"app.config["API_VERSION"] = "v1"app.config["OPENAPI_VERSION"] = "3.1.0"app.config["OPENAPI_URL_PREFIX"] = "/"app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database-file.db"
db.init_app(app)migrate = Migrate(app, db)api = Api(app)api.register_blueprint(CircuitBlueprint)api.register_blueprint(DriverBlueprint)api.register_blueprint(LapTimeBlueprint)The API_TITLE, API_VERSION, and OPENAPI_VERSION keys become the info block in the generated OpenAPI document. The OPENAPI_SWAGGER_UI_PATH and OPENAPI_SWAGGER_UI_URL keys enable the built-in OpenAPI UI.
Adding a server and download endpoint
Server information and a downloadable spec endpoint can be added to app.py:
# Add server information to the OpenAPI specapi.spec.options["servers"] = [ { "url": "http://127.0.0.1:5000", "description": "Local development server" }]
# Serve OpenAPI spec document endpoint for download@app.route("/openapi.yaml")def openapi_yaml(): spec = api.spec.to_dict() return app.response_class( yaml.dump(spec, default_flow_style=False), mimetype="application/x-yaml" )Writing the API
Models
The models.py file defines the SQLAlchemy models. A TimestampMixin adds a created_at field to all models:
from db import dbimport datetime
class TimestampMixin(object): created_at = db.Column(db.DateTime, default=datetime.datetime.now)
class Circuit(TimestampMixin, db.Model): __tablename__ = "circuits" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), nullable=False) location = db.Column(db.String(80), nullable=False)
class Driver(TimestampMixin, db.Model): __tablename__ = "drivers" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), nullable=False) code = db.Column(db.String(80), nullable=False)
class LapTime(TimestampMixin, db.Model): __tablename__ = "lap_times" id = db.Column(db.Integer, primary_key=True) driver_id = db.Column(db.Integer, db.ForeignKey(Driver.id)) circuit_id = db.Column(db.Integer, db.ForeignKey(Circuit.id)) lap_number = db.Column(db.Integer) time_ms = db.Column(db.Integer)Schemas
The schemas.py file defines marshmallow schemas for serialization and deserialization. flask-smorest uses these schemas to validate request data and generate OpenAPI schema components:
from marshmallow import Schema, fields
class CircuitSchema(Schema): id = fields.Int(dump_only=True) name = fields.Str(required=True) location = fields.Str(required=True) created_at = fields.DateTime(dump_only=True)
class DriverSchema(Schema): id = fields.Int(dump_only=True) name = fields.Str(required=True) code = fields.Str(required=True) created_at = fields.DateTime(dump_only=True)
class LapTimeSchema(Schema): id = fields.Int(dump_only=True) driver_id = fields.Int(required=True) circuit_id = fields.Int(required=True) lap_number = fields.Int(required=True) time_ms = fields.Int(required=True) created_at = fields.DateTime(dump_only=True)Fields marked dump_only=True (such as id and created_at) appear in responses but are ignored in request bodies.
Resources
The resources.py file defines the API endpoints using flask-smorest Blueprints and MethodView. Here’s CircuitBlueprint as a representative example — DriverBlueprint and LapTimeBlueprint follow the same pattern:
from flask.views import MethodViewfrom flask_smorest import Blueprint, abortfrom sqlalchemy.exc import IntegrityErrorfrom db import dbfrom models import Circuit, Driver, LapTimefrom schemas import CircuitSchema, DriverSchema, LapTimeSchema
CircuitBlueprint = Blueprint("Circuits", "circuits", url_prefix="/circuits", description="Operations on circuits")
@CircuitBlueprint.route("/")class CircuitList(MethodView): @CircuitBlueprint.response(200, CircuitSchema(many=True)) def get(self): """List all circuits""" return Circuit.query.all()
@CircuitBlueprint.arguments(CircuitSchema) @CircuitBlueprint.response(201, CircuitSchema) def post(self, new_data): """Create a new circuit""" circuit = Circuit(**new_data) db.session.add(circuit) db.session.commit() return circuit
@CircuitBlueprint.route("/<int:circuit_id>")class CircuitDetail(MethodView): @CircuitBlueprint.response(200, CircuitSchema) def get(self, circuit_id): """Get circuit by ID""" return Circuit.query.get_or_404(circuit_id)
@CircuitBlueprint.arguments(CircuitSchema) @CircuitBlueprint.response(200, CircuitSchema) def put(self, updated_data, circuit_id): """Update an existing circuit""" circuit = Circuit.query.get_or_404(circuit_id) circuit.name = updated_data["name"] circuit.location = updated_data["location"] db.session.commit() return circuit
def delete(self, circuit_id): """Delete a circuit""" circuit = Circuit.query.get_or_404(circuit_id) db.session.delete(circuit) db.session.commit() return {"message": "Circuit deleted"}, 204Generating the OpenAPI document
Run the development server to create the database and serve the API:
python app.pyThe following endpoints are now available:
http://127.0.0.1:5000/openapi-ui— OpenAPI UI for interactive documentationhttp://127.0.0.1:5000/openapi.yaml— the downloadable OpenAPI document
To write a static openapi.yaml file to the project root, run:
flask openapi write --format=yaml openapi.yamlThe generated OpenAPI document
Here’s the OpenAPI document flask-smorest generates for the F1 Laps API:
components: responses: DEFAULT_ERROR: content: application/json: schema: $ref: '#/components/schemas/Error' description: Default error response UNPROCESSABLE_ENTITY: content: application/json: schema: $ref: '#/components/schemas/Error' description: Unprocessable Entity schemas: Circuit: properties: created_at: format: date-time readOnly: true type: string id: readOnly: true type: integer location: type: string name: type: string required: - name - location type: object Driver: properties: code: type: string created_at: format: date-time readOnly: true type: string id: readOnly: true type: integer name: type: string required: - name - code type: object Error: properties: code: description: Error code type: integer errors: additionalProperties: {} description: Errors type: object message: description: Error message type: string status: description: Error name type: string type: object LapTime: properties: circuit_id: type: integer created_at: format: date-time readOnly: true type: string driver_id: type: integer id: readOnly: true type: integer lap_number: type: integer time_ms: type: integer required: - circuit_id - driver_id - lap_number - time_ms type: objectinfo: title: F1 Laps API version: v1openapi: 3.1.0paths: /circuits: get: responses: '200': content: application/json: schema: items: $ref: '#/components/schemas/Circuit' type: array description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: List all circuits tags: - Circuits post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Circuit' required: true responses: '201': content: application/json: schema: $ref: '#/components/schemas/Circuit' description: Created '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Create a new circuit tags: - Circuits /circuits{circuit_id}: delete: responses: default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Delete a circuit tags: - Circuits get: responses: '200': content: application/json: schema: $ref: '#/components/schemas/Circuit' description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Get circuit by ID tags: - Circuits parameters: - in: path name: circuit_id required: true schema: minimum: 0 type: integer put: requestBody: content: application/json: schema: $ref: '#/components/schemas/Circuit' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/Circuit' description: OK '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Update an existing circuit tags: - Circuits /drivers: get: responses: '200': content: application/json: schema: items: $ref: '#/components/schemas/Driver' type: array description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: List all drivers tags: - Drivers post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Driver' required: true responses: '201': content: application/json: schema: $ref: '#/components/schemas/Driver' description: Created '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Create a new driver tags: - Drivers /drivers/{driver_id}: delete: responses: default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Delete a driver tags: - Drivers get: responses: '200': content: application/json: schema: $ref: '#/components/schemas/Driver' description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Get driver by ID tags: - Drivers parameters: - in: path name: driver_id required: true schema: minimum: 0 type: integer put: requestBody: content: application/json: schema: $ref: '#/components/schemas/Driver' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/Driver' description: OK '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Update an existing driver tags: - Drivers /lap-times: get: responses: '200': content: application/json: schema: items: $ref: '#/components/schemas/LapTime' type: array description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: List all lap times tags: - LapTimes post: requestBody: content: application/json: schema: $ref: '#/components/schemas/LapTime' required: true responses: '201': content: application/json: schema: $ref: '#/components/schemas/LapTime' description: Created '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Create a new lap time tags: - LapTimes /lap-times/{lap_time_id}: delete: responses: default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Delete a lap time tags: - LapTimes get: responses: '200': content: application/json: schema: $ref: '#/components/schemas/LapTime' description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Get lap time by ID tags: - LapTimes parameters: - in: path name: lap_time_id required: true schema: minimum: 0 type: integer put: requestBody: content: application/json: schema: $ref: '#/components/schemas/LapTime' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/LapTime' description: OK '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Update an existing lap time tags: - LapTimesservers:- description: Local development server url: http://127.0.0.1:5000tags:- description: Operations on circuits name: Circuits- description: Operations on drivers name: Drivers- description: Operations on lap times name: LapTimesAPI metadata
The app.py configuration keys map directly to the info block in the generated document:
app.config["API_TITLE"] = "F1 Laps API"app.config["API_VERSION"] = "v1"app.config["OPENAPI_VERSION"] = "3.1.0"info: title: F1 Laps API version: v1openapi: 3.1.0Server information
The server URL added in app.py appears in the servers block:
servers:- description: Local development server url: http://127.0.0.1:5000Model parameters
Open models.py to see the Circuit model fields:
class Circuit(TimestampMixin, db.Model): __tablename__ = "circuits" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), nullable=False) location = db.Column(db.String(80), nullable=False)Schema in the OpenAPI document
Open openapi.yaml to see the same fields reflected in the Circuit schema:
schemas: Circuit: properties: created_at: format: date-time readOnly: true type: string id: readOnly: true type: integer location: type: string name: type: string required: - name - location type: objectCustomizing the OpenAPI document
The OpenAPI document flask-smorest generates can be enriched with additional detail. Decorators on view methods control both API behavior and the resulting documentation.
Response schema in the OpenAPI document
Use @blp.response() to specify the expected response code and schema for a method:
@CircuitBlueprint.response(200, CircuitSchema(many=True)) def get(self): """List all circuits""" return Circuit.query.all()This generates the following entry for the GET /circuits operation in the OpenAPI document:
/circuits: get: responses: '200': content: application/json: schema: items: $ref: '#/components/schemas/Circuit' type: array description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: List all circuits tags: - CircuitsRequest body in the OpenAPI document
Use @blp.arguments() to validate the request body against a schema and document it in the spec:
@CircuitBlueprint.arguments(CircuitSchema) @CircuitBlueprint.response(201, CircuitSchema) def post(self, new_data): """Create a new circuit""" ...This adds a requestBody to the POST operation:
post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Circuit' required: trueDocstrings in the OpenAPI document
Method docstrings become the summary field in the OpenAPI document. Anything after --- in the docstring is treated as an internal comment and omitted from the spec:
def get(self, circuit_id): """Get circuit by ID. --- Internal note: add caching here later. """ return Circuit.query.get_or_404(circuit_id)This produces:
summary: Get circuit by ID.The internal comment is excluded from the generated document.
Retries in the generated SDK
Global retry behavior for Speakeasy-generated SDKs can be configured by adding an x-speakeasy-retries extension to the OpenAPI spec via app.config["API_SPEC_OPTIONS"]:
app.config["API_SPEC_OPTIONS"] = {"x-speakeasy-retries": { 'strategy': 'backoff', 'backoff': { 'initialInterval': 500, 'maxInterval': 60000, 'maxElapsedTime': 3600000, 'exponent': 1.5, }, 'statusCodes': ['5XX'], 'retryConnectionErrors': True, }}This adds the following to the generated OpenAPI document, which Speakeasy uses when generating SDK retry logic:
x-speakeasy-retries: backoff: exponent: 1.5 initialInterval: 500 maxElapsedTime: 3600000 maxInterval: 60000 retryConnectionErrors: true statusCodes: - 5XX strategy: backoffGenerating SDKs with Speakeasy
With the OpenAPI document ready, Speakeasy can generate client SDKs. First, install the Speakeasy CLI:
curl -fsSL https://go.speakeasy.com/cli-install.sh | shThen run:
speakeasy quickstartFollow the prompts to provide the OpenAPI document location (openapi.yaml), choose a language, and generate. Speakeasy produces a complete client SDK based on your API specification.
Add SDK generation to GitHub Actions
The Speakeasy sdk-generation-action repository provides workflows for integrating the Speakeasy CLI into a CI/CD pipeline, so SDKs are regenerated whenever the OpenAPI document changes.
For an overview of how to set up SDK automation, see the Speakeasy SDK Generation Action and Workflows documentation.
Summary
This guide covered how to generate an OpenAPI document for a Flask API using flask-smorest and use Speakeasy to create client SDKs from it. We covered setting up flask-smorest, writing models, schemas, and blueprint-based resources for the F1 Laps API, generating and inspecting the OpenAPI document, and customizing it with response schemas, request validation, docstrings, and retry configuration.