Path Operation 고급 설정

Path Operation 고급 설정 (Path Operation Advanced Configuration)

이번 장에서는 _path operation_을 문서화할 때 쓸 수 있는 여러 고급 설정들을 살펴볼게요. 대부분은 OpenAPI 스키마를 좀 더 정밀하게 다루고 싶을 때 쓸 수 있어요.

출처: 공식문서

OpenAPI operationId

경고: OpenAPI에 "전문가"가 아니라면 아마 이 기능은 필요 없어요.

_path operation_에 사용할 OpenAPI operationIdoperation_id 파라미터로 설정할 수 있어요.

각 operation마다 고유해야 한다는 점을 확실히 해야 합니다.

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/", operation_id="some_specific_id_you_define")
async def read_items():
    return [{"item_id": "Foo"}]

path operation 함수 이름을 operationId로 사용하기

여러분 API의 함수 이름을 operationId로 쓰고 싶다면, FastAPI에 커스텀 generate_unique_id_function을 전달하면 돼요.

이 함수는 각 APIRoute를 받아 그 path operation에 사용할 operationId를 돌려줍니다.

from fastapi import FastAPI
from fastapi.routing import APIRoute

def custom_generate_unique_id(route: APIRoute) -> str:
    return route.name

app = FastAPI(generate_unique_id_function=custom_generate_unique_id)

@app.get("/items/")
async def read_items():
    return [{"item_id": "Foo"}]

경고: 이렇게 하면 각 _path operation 함수_의 이름이 고유해야 한다는 점을 확실히 해야 해요. 서로 다른 모듈(파이썬 파일)에 있더라도 말이죠.

OpenAPI에서 제외하기

_path operation_을 생성된 OpenAPI 스키마(그리고 자동 문서 시스템)에서 제외하려면 include_in_schema 파라미터를 사용해 False로 설정하면 됩니다:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/", include_in_schema=False)
async def read_items():
    return [{"item_id": "Foo"}]

docstring의 고급 설명

_path operation 함수_의 docstring에서 OpenAPI에 사용할 줄 수를 제한할 수 있어요.

\f(이스케이프된 "form feed" 문자)를 추가하면 FastAPI가 그 지점에서 OpenAPI에 사용할 출력을 잘라냅니다.

그 뒤 내용은 문서에는 안 나타나지만, 다른 도구(예: Sphinx)는 그 나머지를 사용할 수 있어요.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()

@app.post("/items/", summary="Create an item")
async def create_item(item: Item) -> Item:
    """
    Create an item with all the information:

    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    \f
    :param item: User input.
    """
    return item

추가 응답 (Additional Responses)

_path operation_의 response_modelstatus_code를 선언하는 방법은 이미 보셨을 거예요.

그건 _path operation_의 주 응답에 대한 메타데이터를 정의하는 거죠.

추가 응답을 그 모델, 상태 코드 등과 함께 선언할 수도 있어요.

이 문서에 그에 대한 장이 통째로 있어요. OpenAPI의 추가 응답에서 읽을 수 있습니다.

OpenAPI Extra

앱에서 _path operation_을 선언하면 FastAPI는 그 _path operation_에 대한 관련 메타데이터를 자동으로 생성해서 OpenAPI 스키마에 포함시켜요.

기술적 세부사항: OpenAPI 명세에서는 이를 Operation Object라고 불러요. _path operation_에 대한 모든 정보를 담고 있으며 자동 문서 생성에 사용됩니다. tags, parameters, requestBody, responses 등을 포함하죠.

이 _path operation_별 OpenAPI 스키마는 보통 FastAPI가 자동으로 생성하지만, 직접 확장할 수도 있어요.

팁: 이건 저수준 확장 지점이에요. 추가 응답만 선언하면 된다면 OpenAPI의 추가 응답으로 하는 게 더 편해요.

openapi_extra 파라미터를 사용하면 _path operation_의 OpenAPI 스키마를 확장할 수 있습니다.

OpenAPI 확장 (OpenAPI Extensions)

openapi_extra는 예를 들어 OpenAPI Extensions을 선언할 때 유용해요:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/", openapi_extra={"x-aperture-labs-portal": "blue"})
async def read_items():
    return [{"item_id": "portal-gun"}]

자동 API 문서를 열면, 여러분의 확장이 해당 _path operation_의 하단에 나타납니다.

그리고 결과 OpenAPI(여러분 API의 /openapi.json)를 보면, 그 확장이 해당 _path operation_의 일부로 들어가 있는 걸 확인할 수 있어요:

{
    "openapi": "3.1.0",
    "info": {
        "title": "FastAPI",
        "version": "0.1.0"
    },
    "paths": {
        "/items/": {
            "get": {
                "summary": "Read Items",
                "operationId": "read_items_items__get",
                "responses": {
                    "200": {
                        "description": "Successful Response",
                        "content": {
                            "application/json": {
                                "schema": {}
                            }
                        }
                    }
                },
                "x-aperture-labs-portal": "blue"
            }
        }
    }
}

커스텀 OpenAPI path operation 스키마

openapi_extra의 딕셔너리는 _path operation_에 대해 자동 생성된 OpenAPI 스키마와 깊게 병합(deep merge) 됩니다.

그래서 자동 생성된 스키마에 추가 데이터를 넣을 수 있어요.

예를 들어, Pydantic을 쓰는 FastAPI의 자동 기능 대신 자기 코드로 요청을 읽고 검증하기로 결정했더라도, OpenAPI 스키마에는 그 요청을 정의하고 싶을 수 있어요.

그럴 때 openapi_extra로 할 수 있습니다:

from fastapi import FastAPI, Request

app = FastAPI()

def magic_data_reader(raw_body: bytes):
    return {
        "size": len(raw_body),
        "content": {
            "name": "Maaaagic",
            "price": 42,
            "description": "Just kiddin', no magic here. ✨",
        },
    }

@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {
                "application/json": {
                    "schema": {
                        "required": ["name", "price"],
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "price": {"type": "number"},
                            "description": {"type": "string"},
                        },
                    }
                }
            },
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    data = magic_data_reader(raw_body)
    return data

이 예시에서는 Pydantic 모델을 선언하지 않았어요. 실제로 요청 body는 JSON으로 파싱조차 되지 않고 그냥 bytes로 직접 읽히며, magic_data_reader() 함수가 어떤 방식으로든 그걸 파싱하는 역할을 맡게 됩니다.

그럼에도 request body에 대한 예상 스키마는 선언할 수 있어요.

커스텀 OpenAPI 콘텐츠 타입

같은 방법으로 Pydantic 모델을 사용해 JSON Schema를 정의하고, 이를 _path operation_의 커스텀 OpenAPI 스키마 섹션에 넣을 수 있어요.

그리고 요청의 데이터 타입이 JSON이 아니어도 그럴 수 있습니다.

예를 들어, 이 앱에서는 Pydantic 모델에서 JSON Schema를 뽑아내는 FastAPI의 내장 기능도, JSON 자동 검증도 사용하지 않아요. 실제로 요청 콘텐츠 타입을 JSON이 아니라 YAML로 선언하고 있습니다:

import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError

app = FastAPI()

class Item(BaseModel):
    name: str
    tags: list[str]

@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    try:
        data = yaml.safe_load(raw_body)
    except yaml.YAMLError:
        raise HTTPException(status_code=422, detail="Invalid YAML")
    try:
        item = Item.model_validate(data)
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=e.errors(include_url=False))
    return item

그런데 기본 내장 기능을 쓰지 않고 있음에도, YAML로 받고 싶은 데이터의 JSON Schema를 직접 만들기 위해 여전히 Pydantic 모델을 사용하고 있어요.

그 다음 요청을 직접 사용해서 body를 bytes로 추출해요. 즉 FastAPI는 요청 페이로드를 JSON으로 파싱하려고조차 하지 않아요.

그리고 코드에서 그 YAML 내용을 직접 파싱한 뒤, 같은 Pydantic 모델로 YAML 내용을 다시 검증합니다:

import yaml
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError

app = FastAPI()

class Item(BaseModel):
    name: str
    tags: list[str]

@app.post(
    "/items/",
    openapi_extra={
        "requestBody": {
            "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
            "required": True,
        },
    },
)
async def create_item(request: Request):
    raw_body = await request.body()
    try:
        data = yaml.safe_load(raw_body)
    except yaml.YAMLError:
        raise HTTPException(status_code=422, detail="Invalid YAML")
    try:
        item = Item.model_validate(data)
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=e.errors(include_url=False))
    return item

팁: 여기서는 같은 Pydantic 모델을 재사용하고 있어요. 하지만 마찬가지로 다른 방식으로 검증할 수도 있었을 거예요.

더 알아보기 (Learn more)