Skip to content
Platform Status

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’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

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:

Terminal window
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 window
pip install flask-smorest

Configuring flask-smorest

The core configuration for generating an OpenAPI document is added in 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:

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

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:

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 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 window
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 window
flask openapi write --format=yaml openapi.yaml

The 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: 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.config["API_TITLE"] = "F1 Laps API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.1.0"
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:

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

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

@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:
- Circuits

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

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

Generating SDKs with Speakeasy

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

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

Then run:

Terminal window
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.