Skip to content

15. Response Model


1. Response Model

  • 다음과 같은 모든 path operations에서 response_model 매개변수를 사용하여 response에 사용되는 모델을 선언할 수 있다.


1] @app.get()

2] @app.post()

3] @app.put()

4] @app.delete()

5] 기타


from typing import Optional, List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

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

@app.post("/items/", response_model=Item)
async def create_item(item: Item):
    return item


Note

  • 여기서 주의해야 할 것은 response_model은 "데코레이터" 메소드(get, post 등)의 매개변수이며, 모든 매개변수 및 body와 같이 path operation function이 아니라는 것이다.


  • 위의 예제에서는 Pydantic 모델 속성에 대해 선언하는 것과 동일한 유형을 수신하지만, List[Item]과 같은 Pydantic 모델의 list 또한 수신할 수 있다.
  • response_model의 매개변수로 Pydantic 모델을 선언하는 것이 중요한 이유는 출력 데이터를 모델의 데이터로 제한하기 때문이다.


Note

  • response 모델은 함수 반환 타입 어노테이션(여기서는 item: Item) 대신 데코레이터 메소드의 매개변수(여기서는 response_model=Item)로 선언된다.
  • 왜냐하면 path function은 실제로 해당 reponse 모델을 바로 반환하지 않고 dict, 데이터베이스 객체 또는 다른 모델을 반환한 후 response_model을 사용하여 필드 제한 및 직렬화를 수행할 수 있기 때문이다.


2. Return the same input data

  • 다음의 예제는 UserIn 모델을 선언하고, 일반 텍스트 비밀번호를 포함하는 예제이다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None

# Don't do this in production!
@app.post("/user/", response_model=UserIn)
async def create_user(user: UserIn):
    return user


  • 다음과 같이 이 모델을 사용하여 입력을 선언하고, 동일한 모델을 사용하여 출력을 선언한다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None

# Don't do this in production!
@app.post("/user/", response_model=UserIn)
async def create_user(user: UserIn):
    return user


  • 위의 예제를 실행하면, 브라우저가 비밀번호를 포함하여 사용자를 생성할 때마다 API는 response에서 동일한 비밀번호를 반환하게 된다.
  • 이와 같은 경우 결국 사용자 자신이 자신에게 비밀번호를 보내는 것이기 때문에 문제가 되지 않을 수 있다.
  • 하지만 다른 path operation에 동일한 모델을 사용하는 경우 사용자의 비밀번호는 다른 모든 클라이언트에게 보내질 수 있다.


Note

  • 절대 사용자의 비밀번호를 그대로 저장하거나, response로 보내서는 안 된다.


3. Add an output model

  • 위의 예제 대신 일반 텍스트 비밀번호가 있는 입력 모델과 비밀번호가 없는 출력 모델을 만들 수 있다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None

class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None

@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
    return user


  • 위의 예제를 다시 보면, 다음과 같이 path operation function의 반환값으로 함수 반환 타입 어노테이션이 UserInuser를 지정했다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None

class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None

@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
    return user


  • 하지만 다음과 같이 response_modelUserOut이 될 것이기 때문에, 비밀번호를 포함하지 않고 출력할 수 있게 되는 것이다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None

class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None

@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
    return user


4. See it in the docs

  • 자동 문서를 보면 다음과 같이 입력과 출력 모두 자체적인 JSON 스키마를 가지게 된다는 것을 알 수 있다.


001


5. Response Model encoding parameters

  • 다음의 예제는 response 모델에 기본값이 있는 예제이다.


from typing import Optional, List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = 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": []},
}

@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]


  • response 모델에는 다음과 같은 기본값이 지정되어 있다.


1] description: Optional[str] = None

  • 기본값이 None이다.

2] tax: float = 10.5

  • 기본값이 10.5이다.

3] tags: List[str] = []

  • 기본값이 빈 리스트인 []이다.


  • 위와 같이 기본값이 지정되어 있으나, 실제로 클라이언트로부터 저장되지 않은 경우 결과에서 생략할 수 있다.


1) Use the response_model_exclude_unset parameter

  • 결과에서 생략하려면 다음과 같이 path operation 데코레이터 매개변수 response_model_exclude_unset=True를 설정하면 된다.


from typing import Optional, List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = 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": []},
}

@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]


  • 해당 기본값은 response에 포함되지 않고, 실제로 클라이언트로부터 설정된 값만 포함된다.


  • 따라서 ID가 foo인 항목에 대해 해당 path operation에 request를 보내는 경우 response는 다음과 같다.


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


  • response_model_exclude_unset=True뿐만 아니라 다음의 매개변수도 사용할 수 있다.


1] response_model_exclude_defaults=True

2] response_model_exclude_none=True


(1) Data with values for fields with defaults

  • 만약 ID가 bar인 항목과 같이 기본값이 있는 모델이지만 필드에 대한 값이 존재하는 경우, response에 포함될 것이다.


{
  "name": "Bar",
  "description": "The bartenders",
  "price": 62,
  "tax": 20.2
}


(2) Data with the same values as the defaults

  • 만약 ID가 baz인 항목과 같이 데이터가 기본값과 동일한 값을 가지는 경우, 필드에 대한 값이 "명시적"으로 설정되었기 때문에 response에 포함될 것이다.


{
    "name": "Baz",
    "description": None,
    "price": 50.2,
    "tax": 10.5,
    "tags": []
}


2) response_model_include and response_model_exclude

  • path operation 데코레이터 매개변수로 response_model_includeresponse_model_exclude를 사용할 수 있다.
  • 포함하거나 제외할 속성의 이름을 strset로 받게 된다.


  • 다음과 같이 작성할 수 있다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: float = 10.5

items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}

@app.get(
    "items/{item_id}/name",
    response_model=Item,
    response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
    return items[item_id]

@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
    return items[item_id]


  • {"name", "description"} 구문은 이 두 값으로 된 set를 의미한다.
  • 이는 set(["name", "description"])과 동일하다.


Note

  • 포함하거나 제외할 속성을 정할 때 더 좋은 방법은 위와 같은 매개변수를 사용하는 것 대신 여러 클래스를 사용하는 것이다.


(1) Using lists instead of sets

  • 만약 다음과 같이 set가 아닌 list 또는 tuple을 사용해도 FastAPI가 자동으로 변환해 준다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: float = 10.5

items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}

@app.get(
    "items/{item_id}/name",
    response_model=Item,
    response_model_include=["name", "description"],
)
async def read_item_name(item_id: str):
    return items[item_id]

@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
async def read_item_public_data(item_id: str):
    return items[item_id]

References