본문 바로가기

웹 프레임워크/FastAPI

FastAPI - 08 (응답 모델: Response Model)

출처: https://fastapi.tiangolo.com/tutorial/response-model/
아래의 내용은 공식 사이트의 내용을 제 경험과 생각을 추가하여 다시 정리한 것 입니다.

응답 모델 - Return Type

반환 되는 유형을 지정 하는 방법에 대한 예시 입니다.

함수 Return Type에 반환 유형 주석 사용

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []


@app.post("/items/")
# -> 반환 유형 주석 사용, Item을 반환
async def create_item(item: Item) -> Item:  
    return item


@app.get("/items/")
# -> 반환 유형 주석 사용, List[Item]을 반환
async def read_items() -> List[Item]:   
    return [
        Item(name="Portal Gun", price=42.0),
        Item(name="Plumbus", price=32.0),
    ]
http://localhost:8000/items/

위의 url로 접속 하면 아래와 같은 결과가 나옵니다.

[
  {
    "name": "Portal Gun",
    "description": null,
    "price": 42.0,
    "tax": null,
    "tags": []
  },
  {
    "name": "Plumbus",
    "description": null,
    "price": 32.0,
    "tax": null,
    "tags": []
  }
]

만약 위의 소스를 아래와 같이 수정 하여 List[Item]을 반환 하지 않고 문자열을 반환 하도록 수정 합니다.

# ...
# 기존 소스

@app.get("/items/")
async def read_items() -> List[Item]:   # -> 타입 주석 사용, List[Item]을 반환
    # return [
    #     Item(name="Portal Gun", price=42.0),
    #     Item(name="Plumbus", price=32.0),
    # ]
    return "Hello World"

서비스를 다시 실행 시키고 아래의 URL로 접속 해 봅니다.

http://localhost:8000/items/

아래와 같이 에러가 발생 합니다.

Internal Server Error

서버의 로그를 확인 해 보면 아래와 같은 내용이 있습니다.

fastapi.exceptions.ResponseValidationError: 1 validation errors:
  {'type': 'model_attributes_type', 'loc': ('response',), 'msg': 'Input should be a valid dictionary or object to extract fields from', 'input': 'Hello World!', 'url': 'https://errors.pydantic.dev/2.4/v/model_attributes_type'}

response_model 매개변수 사용

아래과 같이 경로 연산자에 response_model 매개변수를 사용하여 반환되는 유형을 지정 할 수 있습니다.

from typing import Any, List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []


# 경로 연산자에 response_model 매개변수로 반환되는 유형(Item)을 지정
@app.post("/items/", response_model=Item)   
# 반환 유형 주석으로 Any를 사용하여 모든 유형을 반환
async def create_item(item: Item) -> Any:
    return item


# 경로 연산자에 response_model 매개변수로 반환되는 유형(List[Item])을 지정
@app.get("/items/", response_model=List[Item])
# 반환 유형 주석으로 Any를 사용하여 모든 유형을 반환
async def read_items() -> Any:   
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]

response_model 매개변수를 사용 할 수 있는 경로 연산자

  • @app.get()
  • @app.post()
  • @app.put()
  • @app.delete()
  • 등.

출력 유형 설정

출력 유형을 별도로 설정 할 수 있습니다.

입력과 동일 유형 반환

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: str
    full_name: Union[str, None] = None


# Don't do this in production!
@app.post("/user/")
# 입력과 동일한 유형(UserIn)을 반환
async def create_user(user: UserIn) -> UserIn:  
    return user

위의 코드에서 반환되는 결과를 확인 하기 위한 테스트 코드(test.py) 입니다.

# 만얀 requests가 설치 되어 있지 않다면 아래의 명령어로 설치 합니다.(CMD 또는 shell )
# pip install requests
import requests

def test_create_user():
    url = "http://localhost:8000/user/"
    data = {
        "username": "testuser",
        "password": "testpassword",
        "email": "test@example.com",
        "full_name": "Test User"
    }

    response = requests.post(url, json=data)
    print("Status Code:", response.status_code)
    print("Response Body:", response.json())

if __name__ == "__main__":
    test_create_user()

CMD 또는 Shell에서 아래의 명령어로 테스트 코드를 실행 합니다.

python test.py
# ubuntu
# python3 test.py

UserIn 유형이 Json으로 반환 되는 것을 확인 할 수 있습니다.

입력과 다른 유형 반환

from typing import Any, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class UserIn(BaseModel):    # 입력 유형
    username: str
    password: str
    email: str
    full_name: Union[str, None] = None


class UserOut(BaseModel):   # 출력 유형
    username: str
    # password는 출력하지 않음
    email: str
    full_name: Union[str, None] = None


# response_model로 출력 유형을 지정
@app.post("/user/", response_model=UserOut)
# 입력 타입은 UserIn
async def create_user(user: UserIn) -> Any:
    return user

테스트 코드는 위와 동일합니다.

import requests

def test_create_user():
    url = "http://localhost:8000/user/"
    data = {
        "username": "testuser",
        "password": "testpassword",
        "email": "test@example.com",
        "full_name": "Test User"
    }

    response = requests.post(url, json=data)
    print("Status Code:", response.status_code)
    print("Response Body:", response.json())

if __name__ == "__main__":
    test_create_user()

테스트 코드를 실행 합니다.

python test.py

UserOut(password가 없는) 유형이 Json으로 반환 되는 것을 확인 할 수 있습니다.

Status Code: 200
Response Body: {'username': 'testuser', 'email': 'test@example.com', 'full_name': 'Test User'}

위와 동일한 다른 코드 예시

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseUser(BaseModel):
    username: str
    email: str
    full_name: Union[str, None] = None


# 입력 유형
# BaseUser를 상속 받고 password를 추가
class UserIn(BaseUser): 
    password: str


@app.post("/user/")
# 입력 유형: UserIn, 출력 유형: BaseUser
async def create_user(user: UserIn) -> BaseUser:
    return user

기타 반환 유형

Response로 반환 유형 설정

from fastapi import FastAPI, Response   # Response 추가
# JSONResponse와 RedirectResponse 추가
from fastapi.responses import JSONResponse, RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response:   # Response 반환
    if teleport:
        # RedirectResponse로 리다이렉트
        return RedirectResponse(url="https://www.youtube.com/watch")
    # JSONResponse로 JSON 반환
    return JSONResponse(content={"message": "Here's your interdimensional portal."})

테스트(teleport가 False인 경우)

http://localhost:8000/portal

아래와 같이 JSONResponse로 JSON이 반환 됩니다.

{"message":"Here's your interdimensional portal."}

테스트(teleport가 True인 경우)

http://localhost:8000/portal?teleport=true

RedirectResponse로
https://www.youtube.com/
로 리다이렉트 됩니다.

Response 하위 클래스로 반환 유형 설정

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/teleport")
# Response 하위 클래스인 RedirectResponse를 반환
async def get_teleport() -> RedirectResponse:
    return RedirectResponse(url="https://www.youtube.com/")

위의 소스는 Response 하위 클래스(RedirectResponse) 반환 타입 주석을 설정 했으므로 정상적으로 작동 합니다.

응답 모델 인코딩 매개변수

응답 모델에서 기본값으로 설정되지 않은 필드를 응답에서 제외 할 수 있습니다.

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5
    tags: List[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


# response_model_exclude_unset=True로 설정하여 
# Key가 설정되지 않은 항목은 제외하고 반환
# 필수 항목이 없으면 오류가 발생
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]

테스트

http://localhost:8000/items/foo

결과

{"name":"Foo","price":50.2}