Skip to content

OpenAPI Documentation

BlackSheep implements automatic generation of OpenAPI Documentation for most common scenarios, and provides methods to enrich the documentation with details. This page describes the following:

  • An introduction to OpenAPI Documentation.
  • Built-in support for OpenAPI Documentation.
  • How to document endpoints.
  • How to handle common responses.
  • Expose the documentation for anonymous access.
  • Support for ReDoc UI.

Introduction to OpenAPI Documentation

Citing from the Swagger web site, at the time of this writing:

The OpenAPI Specification (OAS) defines a standard {...} interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service {...}.

An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.

Since a web application knows by definition the paths it is handling, and since a certain amount of metadata can be inferred from the source code, BlackSheep implements automatic generation of OpenAPI Documentation, and offers an API to enrich the documentation with information that cannot be inferred from the source code.

If you followed the Getting started: MVC tutorial, its project template is configured to include an example of OpenAPI Documentation and to expose a Swagger UI at /docs path.

OpenAPI Docs

Built-in support for OpenAPI Documentation

The following piece of code describes a minimal set-up to enable generation of OpenAPI Documentation and exposing a Swagger UI in BlackSheep:

from dataclasses import dataclass

from blacksheep.server import Application
from blacksheep.server.openapi.v3 import OpenAPIHandler
from openapidocs.v3 import Info

app = Application()

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)


@dataclass
class Foo:
    foo: str


@app.route("/foo")
async def get_foo() -> Foo:
    return Foo("Hello!")

If you start this application and navigate to its /docs route, you will see a Swagger UI like this:

Minimal OpenAPI Setup


In this example, BlackSheep generates this specification file in JSON format, at /openapi.json path:

{
    "openapi": "3.0.3",
    "info": {
        "title": "Example API",
        "version": "0.0.1"
    },
    "paths": {
        "/foo": {
            "get": {
                "responses": {
                    "200": {
                        "description": "Success response",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Foo"
                                }
                            }
                        }
                    }
                },
                "operationId": "get_foo"
            }
        }
    },
    "servers": [],
    "components": {
        "schemas": {
            "Foo": {
                "type": "object",
                "required": [
                    "foo"
                ],
                "properties": {
                    "foo": {
                        "type": "string",
                        "nullable": false
                    }
                }
            }
        }
    }
}

Note how the Foo component schema is automatically documented. BlackSheep supports both @dataclass and Pydantic models for automatic generation of documentation.

And also YAML format at /openapi.yaml path:

openapi: 3.0.3
info:
    title: Example API
    version: 0.0.1
paths:
    /foo:
        get:
            responses:
                '200':
                    description: Success response
                    content:
                        application/json:
                            schema:
                                $ref: '#/components/schemas/Foo'
            operationId: get_foo
servers: []
components:
    schemas:
        Foo:
            type: object
            required:
            - foo
            properties:
                foo:
                    type: string
                    nullable: false

To provide more details for api routes, decorate request handlers using the instance of OpenAPIHandler as a decorator:

@docs(responses={200: "Returns a text saying OpenAPI Example"})
@app.route("/")
def home():
    return "OpenAPI Example"

After this change, the specification file includes the new information:

openapi: 3.0.3
info:
    title: Example API
    version: 0.0.1
paths:
    /:
        get:
            responses:
                '200':
                    description: Returns a text saying OpenAPI Example
            operationId: home
components: {}

Adding description and summary

An endpoint description can be specified either using a docstring:

@docs(responses={200: "Returns a text saying OpenAPI Example"})
@app.route("/")
async def home():
    """
    This example is used to demonstrate support for OpenAPI in BlackSheep.
    The endpoint itself doesn't do anything useful.
    """
    return "OpenAPI Example"

Or in the @docs decorator:

@docs(
    summary="This example is used to demonstrate support for OpenAPI in BlackSheep.",
    description="The endpoint itself doesn't do anything useful.",
    responses={200: "Returns a text saying OpenAPI Example"},
)
@app.route("/")
async def home():
    return "OpenAPI Example"

When using docstring, the first line of the docstring is used as summary, and the whole docstring as description.

OpenAPI description and summary

Note: most of the BlackSheep code base is typed using the typing module, thus IDEs and text editors like Visual Studio Code and PyCharm can provide user's friendly hints for code completion (see the screenshot below).

Type hints

Ignoring endpoints

To exclude certain endpoints from the API documentation, use @docs.ignore():

@docs.ignore()
@app.route("/hidden-from-docs")
async def hidden_endpoint():
    return "This endpoint won't appear in documentation"

Document only certain routes

To document only certain routes, use an include function like in the example below. For example, to include only those routes that starts with "/api":

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))

# include only endpoints whose path starts with "/api/"
docs.include = lambda path, _: path.startswith("/api/")

Documenting response examples

The following example shows how to describe examples for responses:

from dataclasses import dataclass
from datetime import datetime
from uuid import UUID

from blacksheep.server import Application
from blacksheep.server.openapi.common import ContentInfo, ResponseExample, ResponseInfo
from blacksheep.server.openapi.v3 import OpenAPIHandler
from blacksheep.server.responses import json
from openapidocs.v3 import Info

app = Application()

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)


@dataclass
class Cat:
    id: UUID
    name: str
    creation_time: datetime


@docs(
    summary="Gets a cat by id",
    description="""A sample API that uses a petstore as an
          example to demonstrate features in the OpenAPI 3 specification""",
    responses={
        200: ResponseInfo(
            "A cat",
            content=[
                ContentInfo(
                    Cat,
                    examples=[
                        ResponseExample(
                            Cat(
                                id=UUID("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
                                name="Foo",
                                creation_time=datetime.now(),
                            )
                        )
                    ],
                )
            ],
        ),
        404: "Cat not found",
    },
)
@app.route("/api/cats/{cat_id}")
def get_cat_by_id(cat_id: UUID):
    cat = ...  # TODO: implement the logic that fetches a cat by id
    return json(cat)

If the code seems excessively verbose, consider that OpenAPI Specification is designed to support documenting responses with different content types (e.g. JSON, XML, etc.) and having examples for each content type. Writing the specification by hand would be much more time consuming!

BlackSheep automatically generates components schemas by type (in this example, Cat) and reuses them in all API endpoints that use them:

OpenAPI Response Examples

Avoid code pollution using EndpointDocs

If you are familiar with other libraries to produce OpenAPI Documentation and you consider the example above, you might notice that adding OpenAPI details to request handlers can pollute the source code and distract the programmer from the actual request handlers' logic.

BlackSheep provides a way to avoid polluting the source code and keep the code for OpenAPI in dedicated files. Use the blacksheep.server.openapi.common.EndpointDocs class to define documentation in dedicated files and keep your request handlers code clean:

from apidocs.cats import get_cat_docs

@docs(get_cat_docs)
@app.route("/api/cats/{cat_id}")
def get_cat_by_id(cat_id: UUID):
    cat = ...  # TODO: implement the logic that fetches a cat by id
    return json(cat)

To see a complete example, refer to the source code of the MVC project template, and see how documentation is organized and configured (in app.docs, app.controllers.docs).

Deprecated API

To mark and endpoint as deprecated, use @docs.deprecated():

@docs.deprecated()
@app.route("/some-deprecated-api")
async def deprecated_endpoint():
    return "This endpoint is deprecated"

Altering the specification upon creation

To alter the specification file upon creation, define a subclass of OpenAPIHandler that overrides on_docs_generated method.

from blacksheep.server import Application
from blacksheep.server.openapi.v3 import OpenAPIHandler
from blacksheep.server.responses import json
from openapidocs.v3 import Info, OpenAPI, Server

app = Application()


class MyOpenAPIHandler(OpenAPIHandler):
    def on_docs_generated(self, docs: OpenAPI) -> None:
        docs.servers = [
            Server(url="https://foo-example.org"),
            Server(url="https://test.foo-example.org"),
        ]


docs = MyOpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)

Handling common responses

APIs often implement a common way to handle failures, to provide clients with details for web requests that cannot complete successfully. For example, an API might return a response body like the following, in case of a bad request for a certain endpoint:

{"error": "The provided country code is not supported", "code": "InvalidCountryCode"}

Such response body can be handled using a dataclass:

from dataclasses import dataclass


@dataclass
class ErrorInfo:
    error: str
    code: int

blacksheep offers the following way to document common responses:

from openapidocs.v3 import MediaType, Response as ResponseDoc, Schema


error_info = docs.register_schema_for_type(ErrorInfo)

docs.common_responses = {
    400: ResponseDoc(
        "Bad request",
        content={
            "application/json": MediaType(
                schema=Schema(
                    any_of=[error_info],
                    example=SafeException(error="Invalid argument", code=1001),
                )
            )
        },
    ),
    401: ResponseDoc(
        "Unauthorized",
        content={
            "application/json": MediaType(
                schema=Schema(
                    any_of=[error_info],
                    example=SafeException(
                        error="The user is not authorized", code=3
                    ),
                )
            )
        },
    ),
}

Common responses are configured for all endpoints.

Anonymous access

If the server uses a default authorization policy that requires an authenticated user, it is still possible to make the OpenAPI Documentation endpoint available for anonymous access, using the anonymous_access parameter:

from blacksheep.server.openapi.v3 import OpenAPIHandler
from openapidocs.v3 import Info

docs = OpenAPIHandler(
    info=Info(title="Example API", version="0.0.1"), anonymous_access=True
)

# include only endpoints whose path starts with "/api/"
docs.include = lambda path, _: path.startswith("/api/")

Support for ReDoc UI

BlackSheep supports ReDoc UI, although this is disabled by default. It is also possible to implement custom UIs for the documentation endpoints, using the ui_providers property of the OpenAPIHandler class, and implementing a custom UIProvider.

from blacksheep.server.openapi.v3 import OpenAPIHandler
from blacksheep.server.openapi.ui import ReDocUIProvider
from openapidocs.v3 import Info

docs = OpenAPIHandler(
    info=Info(title="Example API", version="0.0.1"),
)

docs.ui_providers.append(ReDocUIProvider())

# include only endpoints whose path starts with "/api/"
docs.include = lambda path, _: path.startswith("/api/")

For more details

For more details on the OpenAPI specification and understand some details such as security settings, refer to the official swagger.io web site, and the dedicated library to generate the specification file: essentials-openapi.