FastAPI × swagger.ymlファイルの出力

## 経緯
Swaggerファイルの修正にかける手間を省略するために、FastAPIのSwagger自動生成機能を使ってみたものの、それまでの開発でSwaggerをymlで統一していたため、ymlファイルとして出力する必要がありました

## FastAPIとは
Pythonで開発された高速なWebフレームワークで、特にAPIの開発に特化しています。

非同期フレームワークであるStarletteと、ValidationやSerializationのためのPydanticを基盤にしており、非常に高速です。

Pythonの型ヒントを活用して、自動的にAPIドキュメントを生成します。Swagger UIやRedocなどのインタラクティブなAPIドキュメントが、開発中に自動で生成されます。

https://fastapi.tiangolo.com/ja/

## Swaggerについて
ローカルでFastAPIが起動している場合、通常は、以下のendpointでswagger情報にアクセスできます
1. Swagger
http://localhost:8000/docs
2. Swagger（JSON）
http://localhost:8000/openapi.json
3. 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は以下のようになっています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536346/39e541c1-9ba0-1767-6a05-bcd493fd0fb6.png)

## 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ファイルが出力されました
<details><summary>swagger.yml</summary>

```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

```
</details>


以下、余計な処理しているので、いい感じにして使ってください。
- 通常はアクセスしないhealthchecke endpointを除外しています
- Pydanticのvalidationエラーが発生した際に返却される422 Unprocessable entityを、400 Bad requestに変換しています