FastAPI × @hey-api/openapi-tsで生成される型定義を確認する

FastAPI × @hey-api/openapi-tsで生成される型定義を確認する

2025.09.02

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

FastAPI+React+Typescriptで開発を行う際、FastAPIが出力するOpenAPIスキーマからフロントで使用する型定義の自動生成に @hey-api/openapi-ts を使用しています。その際 @hey-api/openapi-ts によって生成される型定義の変化が気になったので確認してみました。

前提知識

OpenAPIとは

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

FastAPIとは

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

@hey-api/openapi-tsとは

OpenAPI のスキーマから TypeScript の型やクライアントコードを自動生成するライブラリです。自動生成ライブラリにはほかにもいくつかありますが、比較的新しく、開発が活発なライブラリです。
https://github.com/hey-api/openapi-ts

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

FastAPIで使用する型を定義する

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

例として、UserというモデルをBaseModelを継承して作成し、各フィールド(ここではidやnameのこと)をField関数で定義します。Field関数はデフォルト値やエイリアスなどの制約をカスタマイズするために使用します。

from pydantic import BaseModel, Field

class User(BaseModel):
    id: int = Field()
    name: str = Field()

今回の検証に必要な簡易的なAPIを作成します。

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
    title="Mock Backend",
    description="Backend API",
    version="0.0.1",
    docs_url="/docs",
    openapi_url="/openapi.json",
)

class User(BaseModel):
    id: int = Field()
    name: str = Field()

class ResisterUserInput(BaseModel):
    user: User 

class ResisterUserOutput(BaseModel):
    message: str

@app.post("/user", response_model=ResisterUserOutput)
def post_user(user: ResisterUserInput):
    print(user)

    return ResisterUserOutput(
        message="success"
    )

まずはこの状態の場合、Userモデルはクライアント側ではどのように型生成されるでしょうか。
FastAPIと @hey-api/openapi-ts によるクライアントの型生成の方法については以下のブログに記載していますのでご覧ください。

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

@hey-api/openapi-ts を実行し、生成されたTypescriptの型はこちらになります。

/**
 * User
 */
export type User = {
    /**
     * Id
     */
    id: number;
    /**
     * Name
     */
    name: string;
};

Userモデルのフィールドを様々に変えてみて、@hey-api/openapi-tsによって生成されるTypescriptの型定義を確認することが今回のメインテーマです。

生成される型定義を確認する

デフォルト値の設定

まずUserモデルにデフォルト値を設定したparamを追加します。

class User(BaseModel):
    id: int = Field()
    name: str = Field()
    param: str = Field(default="param")

@hey-api/openapi-ts によって生成される型はこのようになりました。(自動生成されるコメントアウトは省略して記載しています。)
paramの型は str | undefined になりました。

export type User = {
    id: number;
    name: string;
    param?: string;
};

このときFastAPIで生成されるOpenAPIスキーマは以下のようになっており、paramはrequiredではなくなっています。

"User": {
        "properties": {
          "id": { "type": "integer", "title": "Id" },
          "name": { "type": "string", "title": "Name" },
          "param": { "type": "string", "title": "Param", "default": "param" }
        },
        "type": "object",
        "required": ["id", "name"],
        "title": "User"
      },

「Pydantic上でdefault値が付いている → OpenAPIのスキーマではrequiredには含まれない → TS側では任意項目になった」という流れのようです。

オプショナルの設定

UserモデルにOprionalなparamを追加します。

class User(BaseModel):
    id: int = Field()
    name: str = Field()
    param: Optional[str] = Field()

@hey-api/openapi-ts によって生成される型はこのようになりました。

paramの型は string | null になりました。

export type User = {
    id: number;
    name: string;
    param: string | null;
};

このときFastAPIで生成されるOpenAPIスキーマは以下のようになっており、paramはanyOfでstringまたはnullと定義されています。

"User": {
        "properties": {
          "id": { "type": "integer", "title": "Id" },
          "name": { "type": "string", "title": "Name" },
          "param": {
            "anyOf": [{ "type": "string" }, { "type": "null" }],
            "title": "Param"
          }
        },
        "type": "object",
        "required": ["id", "name", "param"],
        "title": "User"
      },

デフォルト値かつオプショナルの設定

Userモデルにデフォルト値があるOprionalなparamを追加します。

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

@hey-api/openapi-ts によって生成される型はこのようになりました。
paramの型は string | null | undefined になりました。

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

このときFastAPIで生成されるOpenAPIスキーマは以下のようになっており、paramはanyOfでstringまたはnullと定義され、requiedではなくなっています。

"User": {
        "properties": {
          "id": { "type": "integer", "title": "Id" },
          "name": { "type": "string", "title": "Name" },
          "param": {
            "anyOf": [{ "type": "string" }, { "type": "null" }],
            "title": "Param",
          }
        },
        "type": "object",
        "required": ["id", "name"],
        "title": "User"
      },

まとめると

  • Optional[T] → TSでは param: T | null
  • default がある→TSでは param?: T

となりました。

Pydantic 定義 OpenAPI スキーマ上の特徴 生成される TypeScript 型 ポイント / 備考
param: str = Field() required に含まれる param: string 必須の文字列
param: str = Field(default="param") required から外れ、default が付与される param?: string default がある → TS 側では undefined 可
param: Optional[str] = Field() anyOf: [string, null] で required 扱い param: string | null Optional でも None デフォルトを付けないと required に含まれる
param: Optional[str] = Field(default=None) anyOf: [string, null]、required から外れる param?: string | null null と undefined の両方を許容する

配列の扱い

paramが配列の場合はどうなるでしょうか。

class User(BaseModel):
    param: List[str] = Field(default_factory=list)

@hey-api/openapi-ts によって生成される型はこのようになりました。
paramの型は string[] | undefined になりました。

export type User = {
    param?: Array<string>;
};

このときFastAPIで生成されるOpenAPIスキーマは以下のようになっており、type=array, items=string として追加されています。

"User": {
        "properties": {
          "param": {
            "items": { "type": "string" },
            "type": "array",
            "title": "Param",
          }
        },
        "type": "object",
        "required": ["id", "name"],
        "title": "User"
      },

整数・浮動小数・真偽値の扱い

整数型と浮動小数型の場合はどうなるでしょうか。

class User(BaseModel):
    param_i: int = Field(default=1)
    param_f: float = Field(default=0.5)
    param_b: bool = Field(default=True)

int型とfloat型はいずれも number | undefined となりました。
bool型は boolean | undefined となりました。

export type User = {
    param_i?: number;
    param_f?: number;
    param_b?: boolean;
};

OpenAPIスキーマ上では区別されています。

"User": {
        "properties": {
          "param_i": { "type": "integer", "title": "param I", "default": 1 },
          "param_f": { "type": "number", "title": "param F", "default": 0.5 },
          "param_b": { "type": "boolean", "title": "Param B", "default": true }
        },
        "type": "object",
        "title": "User"
      },

Enumの扱い

Enumの場合はどうなるでしょうか。

from enum import Enum

class Role(str, Enum):
    admin = "admin"
    user = "user"
    guest = "guest"

class User(BaseModel):
    role: Role

Roleの型は "admin" | "user" | "guest" となり、
Userは Role となりました。
リテラルのユニオン型になるのは使い勝手がよさそうです。

export type Role = 'admin' | 'user' | 'guest';

export type User = {
    role: Role;
};

type=stringでenumが設定されていることがわかります。

"Role": {
        "type": "string",
        "enum": ["admin", "user", "guest"],
        "title": "Role"
      },
"User": {
  "properties": { "role": { "$ref": "#/components/schemas/Role" } },
  "type": "object",
  "required": ["role"],
  "title": "User"
},

日付・時刻の扱い

datetime、date型はどうなるでしょうか。

from datetime import datetime, date

class User(BaseModel):
    created_at: datetime
    birthday: date

いずれもstring型になるので、フロント側で変換して使う必要があります。

export type User = {
    created_at: string;
    birthday: string;
};

OpenAPIのスキーマはこのようになっており、formatで区別されているようです。

"User": {
        "properties": {
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "birthday": {
            "type": "string",
            "format": "date",
            "title": "Birthday"
          }
        },
        "type": "object",
        "required": ["created_at", "birthday"],
        "title": "User"
      },

まとめると以下のようになります。

Pydantic 定義 OpenAPI スキーマ上の特徴 生成される TypeScript 型 ポイント / 備考
param: List[str] = Field(default_factory=list) type: arrayitems: string  required から外れる param?: string[] undefined 許容。null は含まれない
param: int = Field(default=1) type: integer param_i?: number int でも TS 側は number
param: float = Field(default=0.5) type: number param_f?: number float も TS 側は number
param: bool = Field(default=True) type: boolean param_b?: boolean boolean 型になる
param: Role (Enum) class Role(str, Enum): ... type: stringenum: ["admin","user","guest"] "admin" | "user" | "guest" TS側ではリテラルユニオン型に展開される
param: datetime type: string, format: date-time string フロントで Date 等へ変換が必要
param: date type: stringformat: date string 同上、date も string になる

まとめ

今回は、
FastAPI × Pydantic → OpenAPI → @hey-api/openapi-ts → TypeScript型
という流れで、よく使う基本型がどのように変換されるのかを実際に確認してみました。

バックエンド (FastAPI + Pydantic) での型定義が、Typescriptの型にどう反映されるかを体系的に理解しておくと、

  • null / undefined の扱いをコントロールできる
  • 「この型は TS でどう表現されるんだろう?」 と悩まなくて済む

等々恩恵があると思います。

この記事をシェアする

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

© Classmethod, Inc. All rights reserved.