🧩

FastAPIから出力されるOpenAPIをyamlにしたい

に公開

はじめに

FastAPI は python の Web フレームワークです。そして API のインターフェイスを python のコードで記述すれば、API の仕様書を OpenAPI 形式で出力できるという、とても便利な機能を持っています。

しかし標準機能では json 形式の出力のみで、yaml 形式の出力は備わっていません。yaml の方が読みやすくて好きなのでこれは困ります。

この記事では、そんな方向けに yaml で出力する方法を紹介します。また、それ以外にもクライアント側のコード生成を行うにあたって微妙に困ることがあるので、その解決方法も紹介します。

サンプルコードは以下のリポジトリに公開しています。

https://github.com/daikon-oroshi/fastapi-document-custom-sample

yaml 形式での出力

以下のようなコードを追加すると、/openapi.yaml に GET すれば yaml 形式で出力されるようになります。

from fastapi import FastAPI, Response
from functools import lru_cache
import yaml
from yamlcore import CoreDumper


def set_openapi_yaml(app: FastAPI) -> None:

    @lru_cache()
    def read_openapi_yaml() -> Response:
        openapi_json = app.openapi()
        yaml_s = yaml.dump(openapi_json, Dumper=CoreDumper)
        return Response(yaml_s, media_type="text/yaml")

    app.add_api_route(
        "/openapi.yaml",
        read_openapi_yaml,
        methods=["GET"],
        include_in_schema=False,
    )

include_in_schema を False にしているため、このルートは openapi の仕様書に出力されません。FastAPI オブジェクトを set_openapi_yaml に渡すとこの機能がセットされます。

main.py
app = FastAPI()
set_openapi_yaml(app)

Query パラメータのスキーマが出力されない

Query パラメーターは pydantic の Model (以下の例だと SampleQuery) として定義して以下のように書くことができます。

from pydantic import BaseModel

class SampleQuery(BaseModel):
    q_str: str
    q_int: int
from typing import Annotated
from fastapi import Query

@router.get("/")
async def get_sample(query: Annotated[SampleQuery, Query()]):
    pass

しかしこのままでは OpenAPI のスキーマには SampleQuery が出力されません。これはコード生成するときに不便です。

そんな時は以下のようにして解決できます。

def query_schema(query_cls: type[BaseModel]) -> tuple[str, dict[str, Any]]:
    schema = query_cls.model_json_schema(
        ref_template="#/components/schemas/{model}"
    )
    # 余計なので削除
    del schema["$defs"]
    # default null なのに key があると optional にならないのでここで削除
    for value in schema["properties"].values():
        if "default" in value.keys() and value["default"] is None:
            del value["default"]

    return query_cls.__name__, schema
def set_custom_openapi(app: FastAPI) -> None:
    def custom_openapi():
        if app.openapi_schema:
            return app.openapi_schema
        openapi_schema = get_openapi(
            title=app.title,
            version=app.version,
            openapi_version=app.openapi_version,
            summary=app.summary,
            description=app.description,
            terms_of_service=app.terms_of_service,
            contact=app.contact,
            license_info=app.license_info,
            routes=app.routes,
            webhooks=app.webhooks.routes,
            tags=app.openapi_tags,
            servers=app.servers,
            separate_input_output_schemas=app.separate_input_output_schemas,
        )
    # query parameter は schema に追加されないのでここで追加
    queires: list[type[BaseModel]] = [SampleQuery]
    for query in queires:
        q_name, q_schema = query_schema(query)

        openapi_schema["components"]["schemas"][q_name] = q_schema

    return openapi_schema
main.py
app = FastAPI()
set_custom_openapi(app)

すると無事 OpenAPI のスキーマに追加されます。

Enum のスキーマのカスタマイズ

enum の定義に x-enum-varnames を加えるとクライアントのコード生成時に enum で生成してくれます。

PetType:
  type: string
  enum:
    --x-enum-varnames:
    - Cat
    - Dog

以下のように CustomSchemaEnum を定義して、それを継承すると x-enum-varnames が出力されます。

from pydantic_core import core_schema as cs
from pydantic import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from enum import Enum

class CustomSchemaEnum(Enum):
    @classmethod
    def get_names(cls):
        return [e.name for e in cls]

    @classmethod
    def __get_pydantic_json_schema__(
        cls, core_schema: cs.CoreSchema, handler: GetJsonSchemaHandler
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema["x-enum-varnames"] = cls.get_names()
        return json_schema

class PetType(str, CustomSchemaEnum):
    Cat = "猫"
    Dog = "犬"

Redoc のカスタマイズ

ReDoc (デモサイト) は、OpenAPI の仕様書をベースに整った API ドキュメントを生成するドキュメントジェネレーターです。

FastAPI に標準機能として備わっており、サーバーを起動して /redoc にアクセスすれば利用できます。

基本的なカスタマイズ

カスタマイズするには、以下の FastAPI の redoc の html を生成する関数をコピーして中身を変更します。

https://github.com/fastapi/fastapi/blob/26dc148cb91f9ec679b7cf1a74f5a6d6747e7e00/fastapi/openapi/docs.py#L161

ReDoc は <redoc> タグの属性に色々指定して設定を変更できるようです。

https://redocly.com/docs/redoc/deployment/html

<redoc> タグの部分だけ抜粋して、たとえば以下のように変更します。面倒なのでハードコーディングしていますが、それで十分でしょう。

def get_redoc_html(
    openapi_url: str = "/openapi.json",
    theme: dict = None,
) -> HTMLResponse:

    ...省略...
    """"
    <redoc
        spec-url="{openapi_url}"
        disable-search="true"
        hide-download-button="true"
    """
    if theme is not None:
        html += f"""
            theme='{json.dumps(theme)}'
        """
    html += """
    ></redoc>
    """
    ...省略...

指定できるパラメーターは以下のリンクから確認できます。

https://redocly.com/docs-legacy/api-reference-docs/configuration/functionality

そして以下のような関数を用意し設定します。

def set_custom_redoc(app: FastAPI) -> None:
    def redoc_html():
        return get_redoc_html(
            openapi_url=app.openapi_url,
            title=app.title,
            theme={"sidebar": {"backgroundColor": "lightblue"}},
        )

    app.add_api_route(
        "/redoc",
        redoc_html,
        methods=["GET"],
        include_in_schema=False,
    )

redoc_url=None とすることで標準の Redoc が無効になり、カスタマイズした redoc を設定できます。

app = FastAPI(redoc_url=None)
set_custom_redoc(app)

Redoc のロゴ

Redoc のロゴは openapi.yaml の info.x-logo.url にロゴの URL 指定することで変更できます。

info:
  x-logo:
    url: https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png

そのためには FastAPI オブジェクトの openapi メソッドをオーバーライドすれば良いです。

def set_custom_openapi(app: FastAPI) -> None:
    def custom_openapi():
        if app.openapi_schema:
            return app.openapi_schema
        openapi_schema = get_openapi(
            title=app.title,
            version=app.version,
            openapi_version=app.openapi_version,
            summary=app.summary,
            description=app.description,
            terms_of_service=app.terms_of_service,
            contact=app.contact,
            license_info=app.license_info,
            routes=app.routes,
            webhooks=app.webhooks.routes,
            tags=app.openapi_tags,
            servers=app.servers,
            separate_input_output_schemas=app.separate_input_output_schemas,
        )
        openapi_schema["info"]["x-logo"] = {
            "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
        }

    app.openapi = custom_openapi

Swagger UI のカスタマイズ

Swagger UI (デモサイト) は OpenAPI の仕様書をもとに REST API の仕様を可視化し、インタラクティブに操作できるウェブベースのユーザーインターフェースです。

FastAPI の標準機能として備わっており、サーバーを起動して /docs にアクセスすれば利用できます。仕様の確認だけでなく API のテストもできるのでとても便利です。

カスタマイズしたことない (必要性を感じない) のでわかりませんが、ReDoc と同様に以下の関数をコピーして変更すれば良いと思われます。

https://github.com/fastapi/fastapi/blob/26dc148cb91f9ec679b7cf1a74f5a6d6747e7e00/fastapi/openapi/docs.py#L26

GitHubで編集を提案

Discussion