FastAPIで生成されるOpenAPIスキーマを修正する

FastAPIで生成されるOpenAPIスキーマを修正する

2025.09.03

こんにちは!新規事業統括部のこーすけです。

FastAPIで生成されるOpenAPIスキーマを修正したい事案があったのでその備忘録になります。
モチベーションは、FastAPI+React+Typescriptで開発を行っている中で、FastAPIが出力するOpenAPIスキーマを、@hey-api/openapi-tsを使ってTypescriptの型を自動生成する際に、表現したい型を生成できなかったことです。どのようにすれば実現できるかを調査した結果をまとめました。

前提知識

OpenAPIとは

HTTP API を記述するための標準仕様で、エンドポイントやリクエスト・レスポンスの形式を統一的に表現できます。これにより、APIドキュメントの自動生成や、エンジニア間の知識の共有化が容易になり、効率よく開発ができるようになります。

FastAPIとは

Python製の高速な Web フレームワークで、Python標準の型ヒントを活用した自動バリデーションや自動ドキュメント生成が強みです。開発したAPIのエンドポイントや入出力といった仕様をドキュメントに書き起こさなくてよいのは非常に便利です。OpenAPIに準拠したAPIスキーマファイルを生成できます。

@hey-api/openapi-tsとは

OpenAPI のスキーマから TypeScript の型やクライアントコードを自動生成するライブラリです。自動生成ライブラリにはほかにもいくつかありますが、比較的新しく、開発が活発なライブラリです。MITライセンスのため無料で使用可能です。

FastAPIが生成するOpenAPIスキーマを基に@hey-api/openapi-tsでTypescriptの型を生成できます。これによりバックエンドとフロントエンドで型を安全に共有でき、実装の重複や齟齬を防ぎながら効率的に開発を進められます。

https://dev.classmethod.jp/articles/fastapi-hey-api-openapi-ts/
https://dev.classmethod.jp/articles/fastapi-hey-api-openapi-ts-2/

困っていること

FastAPIで入出力やデータ型の定義をする際は、PydanticのBaseModelを継承して作成するのが一般的だと思います。

例えば、Userモデルで name を「デフォルトは NoneOptionalなフィールド」にしたいとします。

from typing import Optional
from pydantic import BaseModel, Field

class User(BaseModel):
    name: Optional[str] = Field(default=None)

OpenAPI スキーマを確認してみる
FastAPI はこのモデルをOpenAPIスキーマへ変換します。生成されたopenapi.json を確認すると、nameフィールドは次のように出力されていました。

"User": {
        "properties": {
          "name": {
            "anyOf": [{ "type": "string" }, { "type": "null" }],
            "title": "Name"
          }
        },
        "type": "object",
        "title": "User"
      },

変換結果
そして、このスキーマを@hey-api/openapi-tsでTypeScriptに変換すると以下のようになります。
@hey-api/openapi-tsの使用方法については上記のブログに記載していますのでご覧下さい。

export type User = {
    name?: string | null;
};

ここで問題になるのが null が必ず含まれてしまう点です。

今回、実現したかった事は、

  • バックエンドでは「デフォルト値は None」にする
  • フロントエンドでは string | undefined にしたい、つまりname?: string としたい

ということですが、 FastAPI → OpenAPI の変換過程で 必ず null を許容する型に展開されてしまうため、最終的に TypeScript 側でも string | null が混ざってしまうという問題がありました。

原因と解決策

原因は、FastAPIがOptional[str] = Field()「null を必ず許可するスキーマ」 として出力してしまう点にあります。

これはどういうことかというと、Optionalなフィールドとして定義した場合、自動生成されるOpenAPIスキーマにはanyOfで{ "type": "null" }が入り込んでしまうため、変換されたTS型では T | nullという型になってしまうのです。

"User": {
        "properties": {
          "name": {
            "anyOf": [{ "type": "string" }, { "type": "null" }],
            "title": "Name"
          }
        },
        "type": "object",
        "title": "User"
      },

この問題は FastAPI が自動で生成する OpenAPI スキーマをカスタマイズして、不要なnullをスキーマから取り除くことで解決できます。

FastAPI では app.openapi() をオーバーライドすることで、生成されるスキーマを加工することが可能です。以下ではOpenAPIスキーマ中にある不要なnullを取り除くカスタム関数を作成し、オーバーライドしました。

def remove_null_in_anyof(schema: dict) -> dict:
    """
    OpenAPIスキーマのanyOfに含まれる {"type": "null"} を除去する。
    再帰的に探索して修正する。
    """
    if isinstance(schema, dict):
        # anyOfをチェック
        if ("anyOf" in schema) and (isinstance(schema["anyOf"], list)):
            # null以外を残す
            non_null = [s for s in schema["anyOf"] if s.get("type") != "null"]
            if len(non_null) == 1:
                schema.clear()
                schema.update(non_null[0])
            else:
                schema["anyOf"] = non_null

        # 各キーを再帰的に処理
        for key, value in schema.items():
            if isinstance(value, (dict, list)):
                schema[key] = remove_null_in_anyof(value)

    elif isinstance(schema, list):
        schema = [remove_null_in_anyof(item) for item in schema]

    return schema

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    openapi_schema = get_openapi(
        title=app.title,
        version=app.version,
        description=app.description,
        routes=app.routes,
    )

    # nullを含むanyOfを全て探索&nullを除去
    openapi_schema = remove_null_in_anyof(openapi_schema)

    app.openapi_schema = openapi_schema
    return app.openapi_schema

app = FastAPI(
    title="Mock Backend",
    description="Backend API",
    version="0.0.1"
)

# オーバーライドする
app.openapi = custom_openapi

このようにapp.openapi()をカスタム関数でオーバーライドすることで、自由に整形することが可能です。生成されたOpenAPIスキーマはこのようになり、{ "type": "null" } を取り除くことができました。

"User": {
        "properties": { "name": { "type": "string" } },
        "type": "object",
        "title": "User"
      },

@hey-api/openapi-tsによって生成される型はこのようになり、今回実現したかったことを達成できました。

export type User = {
    name?: string;
};

まとめ

困っていた点を改めてまとめると、

  • PydanticでOptional[str] = Field(default=None)を書くと、OpenAPI スキーマが anyOf [string, null] に展開されてしまう。
  • そのため、@hey-api/openapi-ts が生成する型はstring | nullまたはstring | null | undefinedとなり、
  • 「バックエンドではデフォルト None、フロントでは string | undefined」という型設計ができなかった。

解決までのアプローチとしては、

  • 原因 : FastAPI が Optional を「null を許す」仕様でスキーマに変換している。
  • 対応 : app.openapi() をオーバーライドし、スキーマから不要な { "type": "null" } を再帰的に取り除く関数を実装した。
  • 結果 : OpenAPI スキーマが"type": "string"に修正され、@hey-api/openapi-ts 側では name?: string (string | undefined) が正しく生成されるようになった。

以上です!カスタム関数を作成すればほかにも様々な問題に対応できそうですね。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.