0
2

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

Posted at

経緯

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

FastAPIとは

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

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

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

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

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に変換しています
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2