Skip to Content
OpenAPI HubFrameworksFlask

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’s Blueprint extends Flask’s built-in blueprint with OpenAPI integration. Blueprints are registered with the Api object instead of the Flask app directly.
  • MethodView: Flask’s class-based views group get, post, put, and delete methods 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

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:

Terminal
python -m venv venv source venv/bin/activate pip install -r requirements.txt

To add flask-smorest to an existing project, install it with:

Terminal
pip install flask-smorest

Configuring flask-smorest

The core configuration for generating an OpenAPI document is added in app.py:

app.py
from flask import Flask from flask_smorest import Api from flask_migrate import Migrate from db import db import models from resources import CircuitBlueprint, DriverBlueprint, LapTimeBlueprint import yaml app = Flask(__name__) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.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:

app.py
# Add server information to the OpenAPI spec api.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:

models.py
from db import db import 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:

schemas.py
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:

resources.py
from flask.views import MethodView from flask_smorest import Blueprint, abort from sqlalchemy.exc import IntegrityError from db import db from models import Circuit, Driver, LapTime from 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"}, 204

Generating the OpenAPI document

Run the development server to create the database and serve the API:

Terminal
python app.py

The following endpoints are now available:

  • http://127.0.0.1:5000/openapi-ui — OpenAPI UI for interactive documentation
  • http://127.0.0.1:5000/openapi.yaml — the downloadable OpenAPI document

To write a static openapi.yaml file to the project root, run:

Terminal
flask openapi write --format=yaml openapi.yaml

The generated OpenAPI document

Here’s the OpenAPI document flask-smorest generates for the F1 Laps API:

openapi.yaml
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: object info: title: F1 Laps API version: v1 openapi: 3.1.0 paths: /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: - LapTimes servers: - description: Local development server url: http://127.0.0.1:5000 tags: - description: Operations on circuits name: Circuits - description: Operations on drivers name: Drivers - description: Operations on lap times name: LapTimes

API metadata

The app.py configuration keys map directly to the info block in the generated document:

app.py
app.config["API_TITLE"] = "F1 Laps API" app.config["API_VERSION"] = "v1" app.config["OPENAPI_VERSION"] = "3.1.0"
openapi.yaml
info: title: F1 Laps API version: v1 openapi: 3.1.0

Server information

The server URL added in app.py appears in the servers block:

openapi.yaml
servers: - description: Local development server url: http://127.0.0.1:5000

Model parameters

Open models.py to see the Circuit model fields:

models.py
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:

openapi.yaml
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

Customizing 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:

resources.py
@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:

openapi.yaml
/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

Request body in the OpenAPI document

Use @blp.arguments() to validate the request body against a schema and document it in the spec:

resources.py
@CircuitBlueprint.arguments(CircuitSchema) @CircuitBlueprint.response(201, CircuitSchema) def post(self, new_data): """Create a new circuit""" ...

This adds a requestBody to the POST operation:

openapi.yaml
post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Circuit' required: true

Docstrings 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:

resources.py
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:

openapi.yaml
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.py
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:

openapi.yaml
x-speakeasy-retries: backoff: exponent: 1.5 initialInterval: 500 maxElapsedTime: 3600000 maxInterval: 60000 retryConnectionErrors: true statusCodes: - 5XX strategy: backoff

Generating SDKs with Speakeasy

With the OpenAPI document ready, Speakeasy can generate client SDKs. First, install the Speakeasy CLI:

Terminal
curl -fsSL https://go.speakeasy.com/cli-install.sh | sh

Then run:

Terminal
speakeasy quickstart

Follow 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.

Last updated on