Export an OpenAPI specification from your FastAPI app

niklasbegley

Nik Begley

Posted on May 19, 2023

Export an OpenAPI specification from your FastAPI app

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),
    ]
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
💖 💪 🙅 🚩
niklasbegley
Nik Begley

Posted on May 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related