
FastAPI × @hey-api/openapi-tsで生成される型定義を確認する
こんにちは!新規事業統括部のこーすけです。
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 の型やクライアントコードを自動生成するライブラリです。自動生成ライブラリにはほかにもいくつかありますが、比較的新しく、開発が活発なライブラリです。
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 によるクライアントの型生成の方法については以下のブログに記載していますのでご覧ください。
@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: array , items: 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: string , enum: ["admin","user","guest"] |
"admin" | "user" | "guest" |
TS側ではリテラルユニオン型に展開される |
param: datetime |
type: string , format: date-time |
string |
フロントで Date 等へ変換が必要 |
param: date |
type: string , format: date |
string |
同上、date も string になる |
まとめ
今回は、
FastAPI × Pydantic → OpenAPI → @hey-api/openapi-ts → TypeScript型
という流れで、よく使う基本型がどのように変換されるのかを実際に確認してみました。
バックエンド (FastAPI + Pydantic) での型定義が、Typescriptの型にどう反映されるかを体系的に理解しておくと、
- null / undefined の扱いをコントロールできる
- 「この型は TS でどう表現されるんだろう?」 と悩まなくて済む
等々恩恵があると思います。