추가 응답
추가 응답 (Additional Responses in OpenAPI)
API를 만들다 보면, 하나의 path operation 이 돌려줄 수 있는 응답이 꼭 한 가지가 아닌 경우가 있어요. 예를 들어 성공했을 때의 200 응답뿐 아니라, 항목을 못 찾았을 때의 404나 검증에 실패했을 때의 422 같은 응답까지 함께 문서화하고 싶을 때가 있죠. 이번 장에서는 FastAPI가 자동으로 만들어 주는 OpenAPI 스키마에 추가 응답(additional response)을 직접 선언해 넣는 방법을 배워요.
경고
이번 주제는 꽤 고급 주제예요.
FastAPI를 막 시작하는 단계라면, 아직 필요하지 않을 수 있어요.
추가 상태 코드, 미디어 타입, 설명 등을 가진 응답을 추가로 선언할 수 있어요. 이렇게 선언한 추가 응답들은 OpenAPI 스키마에 포함되니까 API 문서에도 함께 나타나요.
다만, 이 추가 응답들은 여러분이 Response(예: JSONResponse)를 직접 돌려줘야 해요. 상태 코드와 내용도 직접 지정해서요. 즉 "문서에 이 응답이 있다고 알려줬으니, 실제로는 그 응답을 내 손으로 만들어 돌려줘야 한다"는 뜻이에요.
출처: 공식문서
model로 추가 응답 선언하기
path operation 데코레이터에 responses라는 파라미터를 넘겨줄 수 있어요.
이 파라미터는 dict를 받아요. 각 응답의 키는 상태 코드(예: 200)이고, 값은 그 응답에 대한 정보를 담은 또 다른 dict예요.
그 응답 dict 각각에는 response_model과 마찬가지로 Pydantic 모델을 담는 model 키를 둘 수 있어요.
FastAPI는 그 모델을 가져와서 JSON Schema를 만들고, OpenAPI에서 올바른 위치에 포함시켜 줘요.
예를 들어 상태 코드 404와 Pydantic 모델 Message를 가진 추가 응답을 선언하려면 이렇게 써요.
Python 3.10+
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
class Message(BaseModel):
message: str
app = FastAPI()
@app.get("/items/{item_id}", response_model=Item, responses={404: {"model": Message}})
async def read_item(item_id: str):
if item_id == "foo":
return {"id": "foo", "value": "there goes my hero"}
return JSONResponse(status_code=404, content={"message": "Item not found"})
참고
JSONResponse를 직접 돌려줘야 한다는 점을 꼭 기억하세요.
참고
model키는 OpenAPI의 일부가 아니에요.FastAPI가 그 위치에서 Pydantic 모델을 꺼내 JSON Schema를 생성하고 올바른 위치에 넣어 주는 거예요.
올바른 위치는 이렇게 생겼어요.
content키 안에. 이 키의 값은 또 다른 JSON 객체(dict)이고, 그 안에는:- 미디어 타입 키(예:
application/json)가 있고, 이 키의 값은 또 다른 JSON 객체이고, 그 안에는:schema키가 있는데, 그 값이 바로 모델의 JSON Schema예요. 이게 올바른 위치예요.- 여기서 FastAPI는 JSON Schema를 직접 넣는 대신 OpenAPI의 다른 위치에 있는 전역 JSON Schema를 가리키는 참조(reference)를 추가해요. 이렇게 하면 다른 애플리케이션과 클라이언트가 그 JSON Schema를 직접 사용할 수 있고, 더 나은 코드 생성 도구를 제공할 수 있기 때문이에요.
- 미디어 타입 키(예:
이 path operation 에 대해 OpenAPI에 생성되는 응답은 이렇게 생겨요.
{
"responses": {
"404": {
"description": "Additional Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Message"
}
}
}
},
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
스키마들은 OpenAPI 스키마 안의 다른 위치를 참조하고 있어요.
{
"components": {
"schemas": {
"Message": {
"title": "Message",
"required": [
"message"
],
"type": "object",
"properties": {
"message": {
"title": "Message",
"type": "string"
}
}
},
"Item": {
"title": "Item",
"required": [
"id",
"value"
],
"type": "object",
"properties": {
"id": {
"title": "Id",
"type": "string"
},
"value": {
"title": "Value",
"type": "string"
}
}
},
"ValidationError": {
"title": "ValidationError",
"required": [
"loc",
"msg",
"type"
],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"type": "string"
}
},
"msg": {
"title": "Message",
"type": "string"
},
"type": {
"title": "Error Type",
"type": "string"
}
}
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {
"$ref": "#/components/schemas/ValidationError"
}
}
}
}
}
}
}
주요 응답에 추가 미디어 타입 넣기
같은 responses 파라미터를 이용해서, 주요 응답에 서로 다른 미디어 타입을 추가할 수도 있어요.
예를 들어 image/png라는 추가 미디어 타입을 넣어서, path operation 이 JSON 객체(미디어 타입 application/json) 또는 PNG 이미지를 돌려줄 수 있다고 선언할 수 있어요.
Python 3.10+
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
app = FastAPI()
@app.get(
"/items/{item_id}",
response_model=Item,
responses={
200: {
"content": {"image/png": {}},
"description": "Return the JSON item or an image.",
}
},
)
async def read_item(item_id: str, img: bool | None = None):
if img:
return FileResponse("image.png", media_type="image/png")
else:
return {"id": "foo", "value": "there goes my hero"}
참고
이미지는
FileResponse를 사용해 직접 돌려줘야 한다는 점에 주의하세요.
참고
responses파라미터에서 다른 미디어 타입을 명시적으로 지정하지 않으면, FastAPI는 응답이 주요 응답 클래스와 같은 미디어 타입을 가진다고 가정해요(기본값은application/json).하지만 미디어 타입이
None인 커스텀 응답 클래스를 지정했다면, FastAPI는 모델과 연결된 추가 응답에 대해application/json을 사용해요.
정보 합치기
response_model, status_code, responses 등 여러 곳에서 온 응답 정보를 합칠 수도 있어요.
response_model을 기본 상태 코드 200(필요하면 커스텀 코드)으로 선언하고, 같은 응답에 대한 추가 정보를 responses에서 OpenAPI 스키마에 직접 선언할 수 있어요.
FastAPI는 responses의 추가 정보를 유지하면서, 여러분 모델의 JSON Schema와 결합해 줘요.
예를 들어 Pydantic 모델을 사용하고 커스텀 description을 가진 상태 코드 404 응답을 선언할 수 있어요.
그리고 response_model을 사용하면서 커스텀 example을 포함한 상태 코드 200 응답도 선언할 수 있어요.
Python 3.10+
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
class Message(BaseModel):
message: str
app = FastAPI()
@app.get(
"/items/{item_id}",
response_model=Item,
responses={
404: {"model": Message, "description": "The item was not found"},
200: {
"description": "Item requested by ID",
"content": {
"application/json": {
"example": {"id": "bar", "value": "The bar tenders"}
}
},
},
},
)
async def read_item(item_id: str):
if item_id == "foo":
return {"id": "foo", "value": "there goes my hero"}
else:
return JSONResponse(status_code=404, content={"message": "Item not found"})
이 모든 정보가 합쳐져 OpenAPI에 포함되고 API 문서에도 표시돼요.
미리 정의된 응답과 커스텀 응답 합치기
여러 path operation 에 적용되는 미리 정의된 응답들을 만들어 두고, 각 path operation 이 필요로 하는 커스텀 응답과 합치고 싶을 수도 있어요.
그런 경우에는 파이썬의 **dict_to_unpack을 이용한 dict "언패킹" 기법을 사용할 수 있어요.
old_dict = {
"old key": "old value",
"second old key": "second old value",
}
new_dict = {**old_dict, "new key": "new value"}
여기서 new_dict는 old_dict의 모든 키-값 쌍에 새 키-값 쌍이 더해진 것들을 담고 있어요.
{
"old key": "old value",
"second old key": "second old value",
"new key": "new value",
}
이 기법을 사용해서 path operations 에서 미리 정의된 응답 몇 개를 재사용하고, 여기에 추가로 필요한 커스텀 응답들을 합칠 수 있어요.
예를 들어 이렇게요.
Python 3.10+
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
responses = {
404: {"description": "Item not found"},
302: {"description": "The item was moved"},
403: {"description": "Not enough privileges"},
}
app = FastAPI()
@app.get(
"/items/{item_id}",
response_model=Item,
responses={**responses, 200: {"content": {"image/png": {}}}},
)
async def read_item(item_id: str, img: bool | None = None):
if img:
return FileResponse("image.png", media_type="image/png")
else:
return {"id": "foo", "value": "there goes my hero"}
OpenAPI 응답에 대한 더 많은 정보
responses에 정확히 무엇을 넣을 수 있는지 보려면 OpenAPI 명세의 다음 섹션들을 확인해 보세요.
- OpenAPI Responses Object — 이 객체 안에
Response Object가 포함돼 있어요. - OpenAPI Response Object —
responses파라미터 안의 각 응답에 이 객체의 어떤 것이라도 직접 넣을 수 있어요.description,headers,content(이 안에 서로 다른 미디어 타입과 JSON Schema를 선언하죠),links등이 포함돼요.
더 알아보기 (Learn more)
이 문서는 FastAPI 공식 문서 - Additional Responses in OpenAPI를 한국어로 정리한 번역이에요. 원문에서 최신 내용과 더 다양한 예시를 확인하세요.