経緯
Swaggerファイルの修正にかける手間を省略するために、FastAPIのSwagger自動生成機能を使ってみたものの、それまでの開発でSwaggerをymlで統一していたため、ymlファイルとして出力する必要がありました
FastAPIとは
Pythonで開発された高速なWebフレームワークで、特にAPIの開発に特化しています。
非同期フレームワークであるStarletteと、ValidationやSerializationのためのPydanticを基盤にしており、非常に高速です。
Pythonの型ヒントを活用して、自動的にAPIドキュメントを生成します。Swagger UIやRedocなどのインタラクティブなAPIドキュメントが、開発中に自動で生成されます。
Swaggerについて
ローカルでFastAPIが起動している場合、通常は、以下のendpointでswagger情報にアクセスできます
- Swagger
http://localhost:8000/docs - Swagger(JSON)
http://localhost:8000/openapi.json - Redoc
http://localhost:8000/redoc
mainコードサンプル
FastAPIで簡単なcrudを行うAPIのサンプルを以下に示します
main.py
from fastapi import FastAPI, APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, List
app = FastAPI()
router = APIRouter()
# リクエスト用のデータモデル
class ItemRequestModel(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
# レスポンス用のデータモデル
class ItemResponseModel(BaseModel):
id: int
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
total: float
def calculate_total(self):
return self.price + (self.tax or 0)
# 仮のデータベース
fake_db = {}
# アイテム作成
@router.post("/items/{item_id}", response_model=ItemResponseModel)
async def create_item(item_id: int, item: ItemRequestModel):
if item_id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
total = item.price + (item.tax or 0)
item_data = ItemResponseModel(id=item_id, **item.dict(), total=total)
fake_db[item_id] = item_data
return item_data
# アイテム取得
@router.get("/items/{item_id}", response_model=ItemResponseModel)
async def read_item(item_id: int):
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
# 全アイテム取得
@router.get("/items", response_model=List[ItemResponseModel])
async def read_all_items():
return list(fake_db.values())
# アイテム更新
@router.put("/items/{item_id}", response_model=ItemResponseModel)
async def update_item(item_id: int, item: ItemRequestModel):
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
total = item.price + (item.tax or 0)
item_data = ItemResponseModel(id=item_id, **item.dict(), total=total)
fake_db[item_id] = item_data
return item_data
# アイテム削除
@router.delete("/items/{item_id}", response_model=dict)
async def delete_item(item_id: int):
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
del fake_db[item_id]
return {"detail": "Item deleted"}
# ルーターをアプリケーションに登録
app.include_router(router, prefix="/api")
# health check用endpoint
@app.get("/healthcheck")
async def healthcheck():
return
この状態で、/docsにアクセスした場合のUIは以下のようになっています
Swagger.ymlの出力
app.openapi()でswagger情報が取り出せることを利用して、ymlファイルを出力します
※FastAPIが起動している必要はありません
output_swagger.py
import sys
import yaml
sys.path.append("/usr/src/app")
from app.main_api import app # noqa: E402
def format_paths(paths: dict) -> dict:
def format_response(responses: dict) -> dict:
return {status_code if status_code != "422" else "400": contents for status_code, contents in responses.items()}
results = {}
for path, methods in paths.items():
if path == "/healthcheck":
continue
result = {}
for method, items in methods.items():
if items.get("responses"):
result[method] = {**items, "responses": format_response(items["responses"])}
else:
result[method] = items
results[path] = result
return results
if __name__ == "__main__":
api_json = app.openapi()
formatted_api_json = {**api_json, "paths": format_paths(api_json["paths"])}
with open("swagger.yml", mode="w") as f:
f.write(yaml.dump(formatted_api_json, Dumper=yaml.Dumper, allow_unicode=True))
以下のymlファイルが出力されました
swagger.yml
swagger.yml
components:
schemas:
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
title: Detail
type: array
title: HTTPValidationError
type: object
ItemRequestModel:
properties:
description:
anyOf:
- type: string
- type: 'null'
title: Description
name:
title: Name
type: string
price:
title: Price
type: number
tax:
anyOf:
- type: number
- type: 'null'
title: Tax
required:
- name
- price
title: ItemRequestModel
type: object
ItemResponseModel:
properties:
description:
anyOf:
- type: string
- type: 'null'
title: Description
id:
title: Id
type: integer
name:
title: Name
type: string
price:
title: Price
type: number
tax:
anyOf:
- type: number
- type: 'null'
title: Tax
total:
title: Total
type: number
required:
- id
- name
- price
- total
title: ItemResponseModel
type: object
ValidationError:
properties:
loc:
items:
anyOf:
- type: string
- type: integer
title: Location
type: array
msg:
title: Message
type: string
type:
title: Error Type
type: string
required:
- loc
- msg
- type
title: ValidationError
type: object
info:
title: FastAPI
version: 0.1.0
openapi: 3.1.0
paths:
/api/items:
get:
operationId: read_all_items_api_items_get
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/ItemResponseModel'
title: Response Read All Items Api Items Get
type: array
description: Successful Response
summary: Read All Items
/api/items/{item_id}:
delete:
operationId: delete_item_api_items__item_id__delete
parameters:
- in: path
name: item_id
required: true
schema:
title: Item Id
type: integer
responses:
'200':
content:
application/json:
schema:
title: Response Delete Item Api Items Item Id Delete
type: object
description: Successful Response
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Delete Item
get:
operationId: read_item_api_items__item_id__get
parameters:
- in: path
name: item_id
required: true
schema:
title: Item Id
type: integer
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ItemResponseModel'
description: Successful Response
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Read Item
post:
operationId: create_item_api_items__item_id__post
parameters:
- in: path
name: item_id
required: true
schema:
title: Item Id
type: integer
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ItemRequestModel'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ItemResponseModel'
description: Successful Response
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Create Item
put:
operationId: update_item_api_items__item_id__put
parameters:
- in: path
name: item_id
required: true
schema:
title: Item Id
type: integer
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ItemRequestModel'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ItemResponseModel'
description: Successful Response
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Update Item
以下、余計な処理しているので、いい感じにして使ってください。
- 通常はアクセスしないhealthchecke endpointを除外しています
- Pydanticのvalidationエラーが発生した際に返却される422 Unprocessable entityを、400 Bad requestに変換しています