Nik Begley
Posted on May 19, 2023
FastAPI is a modern Python web framework for building APIs. FastAPI is a great choice for building simple APIs, and it comes with built-in support for generating OpenAPI documentation.
In this post we will look at how to generate and extract the OpenAPI specification from a FastAPI project.
A simple single-file FastAPI example taken from the official docs will suffice as our testbed. The same workflow will also work for larger projects with routers.
Step 1: Add Tags and Metadata
Chances are that you'll use the OpenAPI spec to generate documentation or code. It is important to add metadata to your FastAPI app so that the generated OpenAPI spec is complete.
Firstly, you should tag our endpoints with tags
to make sure they are grouped in logical operations. This example does not use routers, but if you do, you need to tag the router instead of the endpoint.
Tags are used by documentation and code generators to group endpoints together. Tags may include spaces and special characters, but we recommend to keep the tags simple. It is common to use either lowercase or Capital Case for tags, like Items
in our example.
In addition to tags, we'll add a description
and version
metadata to our FastAPI app instance. The description
and version
will be used in the generated OpenAPI docs on the overview page. You can find the full list of metadata parameters in the FastAPI docs if you need to include additional details in your specification.
The full example main.py
now looks like this:
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(description="Example app", version="0.1.0")
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: list[str] = []
@app.post("/items/", tags=["Items"])
async def create_item(item: Item) -> Item:
return item
@app.get("/items/", tags=["Items"])
async def read_items() -> list[Item]:
return [
Item(name="Portal Gun", price=42.0),
Item(name="Plumbus", price=32.0),
]
This example requires pip install fastapi[all]
and pip install pydantic
to run.
Step 2: Create the Export Script
By default FastAPI will generate OpenAPI docs under /docs
. You can try this out by running the app and navigating to http://localhost:8000/docs
.
It is possible to get the OpenAPI JSON directly by navigating to /openapi.json
, but we'll want to extract the document programmatically in order to be able to automate the process. FastAPI does not support exporting the OpenAPI specification directly, but we'll use a small script to extract it.
Create a file extract-openapi.py
with the following contents:
# extract-openapi.py
import argparse
import json
import sys
import yaml
from uvicorn.importer import import_from_string
parser = argparse.ArgumentParser(prog="extract-openapi.py")
parser.add_argument("app", help='App import string. Eg. "main:app"', default="main:app")
parser.add_argument("--app-dir", help="Directory containing the app", default=None)
parser.add_argument("--out", help="Output file ending in .json or .yaml", default="openapi.yaml")
if __name__ == "__main__":
args = parser.parse_args()
if args.app_dir is not None:
print(f"adding {args.app_dir} to sys.path")
sys.path.insert(0, args.app_dir)
print(f"importing app from {args.app}")
app = import_from_string(args.app)
openapi = app.openapi()
version = openapi.get("openapi", "unknown version")
print(f"writing openapi spec v{version}")
with open(args.out, "w") as f:
if args.out.endswith(".json"):
json.dump(openapi, f, indent=2)
else:
yaml.dump(openapi, f, sort_keys=False)
print(f"spec written to {args.out}")
What this script does is import the app from the given import string, and then call app.openapi()
to get the OpenAPI spec. The spec is then written to the given output file.
You can invoke help to see the available options:
$ python3 export-openapi.py --help
usage: extract-openapi.py [-h] [--app-dir APP_DIR] [--out OUT] app
positional arguments:
app App import string. Eg. "main:app"
options:
-h, --help show this help message and exit
--app-dir APP_DIR Directory containing the app
--out OUT Output file ending in .json or .yaml
If you have trouble with the uvicorn
import, make sure you have installed the fastapi[all]
package, which includes uvicorn, or that you have uvicorn
installed.
Step 3: Export OpenAPI spec from FastAPI
Running the script
To run the export script, you need to know the import string of your FastAPI app. The import string is is what you pass to uvicorn when running your app, like uvicorn main:app
.
This depends on where your code is located, and what the FastAPI instance is called. See below for tips on how to determine the import string.
# Run the script by passing the import string of your FastAPI app
# If you don't know the import string, see below for examples
$ python extract-openapi.py main:app
This should create an openapi.json
or openapi.yaml
file in your current directory.
Example case: simple project structure
In our example, we have a project structure like this:
/my_project
├── extract-openapi.py
└── main.py
Our FastAPI instance is app
since we wrote app = FastAPI()
in main.py
. We also have a single file main.py
in the current directory, so the import string is main:app
.
To extract the OpenAPI spec we do
$ python extract-openapi.py main:app
Example case: nested project structure
For larger applications, the project structure is often more nested, like this:
/my_project
├── extract-openapi.py
└── myapp
└── main.py
In this case we'll have to add the module name, myapp
, to the import string. The import string is now myapp.main:app
. Alternatively, if you are having trouble with this, you can use the --app-dir
argument to specify the directory containing the entrypoint of your application.
In this case, to extract the OpenAPI spec we do
$ python extract-openapi.py myapp.main:app
# or alternatively
$ python extract-openapi.py --app-dir myapp main:app
You should now have the openapi.json
or openapi.yaml
file in your current directory.
Step 4: Automate in your CI/CD pipeline (optional)
How you'll integrate the extraction to your CI/CD depends on what you are trying to accomplish. The three most common ways to approach this are:
- Extract the spec locally and commit it to your repository. Let CI/CD verify the committed spec is up-to-date.
- Extract the spec as part of your CI/CD pipeline, and use the spec as a temporary file to accomplish something (eg. generate a client).
- Extract the spec as part of your CI/CD pipeline, and commit the generated spec to your repository, when merging to main.
The benefit with using a script is that it can also be run locally. Locally committing is often a safe and straight-forward approach, but may occasionally make merging more difficult. If, however, you only need to generate the OpenAPI spec as part of your CI/CD pipeline, you should also consider dedicated GitHub Actions.
As an example, we'll demonstrate a GitHub Actions job, which verifies that the committed spec matches a generated one:
# .github/workflows/main.yml
name: CI
on: [push]
jobs:
extract-openapi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.11
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Extract OpenAPI spec
run: python extract-openapi.py main:app --out openapi_generated.yaml
# Do something with the generated spec here.
# For example, validate that the committed spec matches the generated one.
- name: Verify OpenAPI spec has been updated
run: git diff --exit-code openapi.yaml openapi_generated.yaml
Summary
In summary, we have
- Added
tags
to each endpoint or router - Added
description
,version
and other metadata to our FastAPI app instance - Created a script
extract-openapi.py
to extract the OpenAPI spec from FastAPI - Automated the extraction in CI/CD pipeline
Posted on May 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.