Path Operation 고급 설정
Path Operation 고급 설정 (Path Operation Advanced Configuration)
이번 장에서는 _path operation_을 문서화할 때 쓸 수 있는 여러 고급 설정들을 살펴볼게요. 대부분은 OpenAPI 스키마를 좀 더 정밀하게 다루고 싶을 때 쓸 수 있어요.
출처: 공식문서
OpenAPI operationId
경고: OpenAPI에 "전문가"가 아니라면 아마 이 기능은 필요 없어요.
_path operation_에 사용할 OpenAPI operationId를 operation_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_model과 status_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 모델을 재사용하고 있어요. 하지만 마찬가지로 다른 방식으로 검증할 수도 있었을 거예요.