https://fastapi.tiangolo.com/tutorial/body-multiple-params/
이전에 POST
, PUT
, DELETE
http method들은 handler의 parameter가 BaseModel
을 상속한 객체라면 이를 request body로 해석한다고 했다. 이 덕분에 fastapi는 query, path, body parameter를 handler에 혼용으로써도 문제없이 구별해낼 수 있다.
from typing import Annotated, Union
from fastapi import FastAPI, Path
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
@app.put("/items/{item_id}")
async def update_item(
item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)],
q: Union[str, None] = None,
item: Union[Item, None] = None,
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
if item:
results.update({"item": item})
return results
다음의 코드에서 update_item
handler는 path parameter로 item_id
를 갖고, query parameter로 q
를 갖으며 request body로 item
을 갖는다. 여기서 item
은 타입이 Item
이며 pydantic의 BaseModel
을 상속했기 때문에 request body로 받아들일 수 있는 것이다.
요청을 진행해보도록 하자.
curl -X PUT -H "Content-Type: application/json" "localhost:8888/items/12?q=hello" -d '{"name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2}'
{"item_id":12,"q":"hello","item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2}}
성공적으로 받은 것을 볼 수 있다.
그렇다면 다음의 client request body는 어떻게 받아낼 수 있을까?
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
}
}
하나의 JSON에 두 개의 key로 item
과 user
가 있는 것이다. 단편적으로 떠오르는 생각은 하나의 JSON에 item
과 user
를 받아야하므로 BaseModel
을 상속받은 python class를 item
과 user
field를 갖도록 하는 것이다.
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class User(BaseModel):
username: str
full_name: Union[str, None] = None
class ItemAndUser(BaseModel):
item: Item
user: User
ItemAndUser
class model은 item
과 user
을 각각 받아낼 수 있을 것이다.
그러나 위와 같이 사용하지 않고 fastapi에서 이를 각각 받아내는 기능을 제공해준다.
from typing import Annotated, Union
from fastapi import FastAPI, Path
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class User(BaseModel):
username: str
full_name: Union[str, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
results = {"item_id": item_id, "item": item, "user": user}
return results
update_item
handler는 item
과 user
request body를 parameter로 각각 받아내고 있다. 즉, 하나의 JSON에 서로 다른 field라도 이름을 지정하고 BaseModel
을 상속하는 pydantic class model을 만든다면 fastapi에서 이를 자동으로 검출해내는 것이다.
요청을 보내보도록 하자.
curl -X PUT -H "Content-Type: application/json" "localhost:8888/items/12" -d '{"item":{"name":"Foo","description":"The pretender","price":42,"tax":3.2},"user":{"username":"dave","full_name":"Dave Grohl"}}'
{"item_id":12,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2},"user":{"username":"dave","full_name":"Dave Grohl"}}
잘 적용된 것을 볼 수 있다.
이렇게 fastapi에서 하나의 JSON객체에 있는 서로 다른 field가 있다면, handler의 parameter로 BaseModel
을 상속한 class model로만 정의를 해 각각을 받아낼 수 있다는 것을 알 수 있다. 이렇게하면 이전처럼 ItemAndUser
와 같은 추가적인 model class를 만들 필요가 없다.
request body도 singular body로 받아낼 수 있다. 즉 int
, float
, str
등으로 받아낼 수 있다는 것이다. 다만, 그대로 쓸 수는 없는데, 왜냐하면 query parameter와 헷갈릴 수 있기 때문이다. 따라서 Body
객체를 이용해서 해당 parameter가 request body라는 것을 지시해주어야 한다.
가령, importance
라는 body parameter를 쓰고 싶은데 int
타입이라서 BaseModel
를 상속받은 class model까지 쓰고 싶지않다면 Body
로 importance
라는 parameter가 query parameter가 아니라 body parameter임을 나타내야 한다.
from typing import Annotated, Union
from fastapi import FastAPI, Body
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class User(BaseModel):
username: str
full_name: Union[str, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User, importance: Annotated[int, Body()]):
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
return results
importance: Annotated[int, Body()]
를 보면 importance
가 body parameter임을 알리기위해서 Body()
를 사용한 것을 볼 수 있다.
curl -X PUT -H "Content-Type: application/json" "localhost:8888/items/12" -d '{"item":{"name":"Foo","description":"The pretender","price":42,"tax":3.2},"user":{"username":"dave","full_name":"Dave Grohl"},"importance":5}'
{"item_id":12,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2},"user":{"username":"dave","full_name":"Dave Grohl"},"importance":5}
제대로 요청을 처리한 것을 볼 수 있다.
따라서 singular type으로 parameter를 사용할 때는 기본적으로 fastapi가 query parameter로 치부하기 때문에 Body
를 사용해서 해당 parameter가 body parameter임을 명백하게 알려주는 것이 필요하다.
만약 body parameter가 오직 하나의 item
field만을 갖고있다면 어떻게 request body를 만들어야할까? 가령 다음과 같은 요청이 온다고 하자.
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
}
이를 받아내는 모델은 다음과 같다.
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class ItemModel(BaseModel):
item: Item
다음과 같이 ItemModel
을 따로 만들고 Item
타입을 갖는 item
field를 만드는 것이 최선일 것이다.
그러나, 실제적으로 원하는 것은 ItemModel
의 item
field 내부의 값들이다. 이 값들을 얻기위해서 ItemModel
을 따로 만드는 것 자체도 굉장히 귀찮은 일이다. fastapi에서는 이를 위해서 Embed
옵션을 제공해주는데, Embed
는 안에 있는 field를 가져와주는 기능을 한다. Body(embed=True)
로 하면되고, Embed
가 True
가 되면, 오직 하나의 field와 이름이 같은 body parameter에 해당 field의 내부 field들이 할당된다.
즉, 위의 예제로보면 JSON객체의 item
field와 이름이 동일한 item
body parameter를 만들고 Body(embed=True)
로 감싸주면 body parameter의 item
은 JSON객체의 item
field 내부의 값들이 할당될 것이다.
from typing import Annotated, Union
from fastapi import FastAPI, Body
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
results = {"item_id": item_id, "item": item}
return results
요청을 보내보도록 하자.
curl -X PUT -H "Content-Type: application/json" "localhost:8888/items/12" -d '{"item":{"name":"Foo","description":"The pretender","price":42,"tax":3.2}}'
{"item_id":12,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2}}
문제 없이 받아내고 item
body parameter는 client요청에 있던 JSON객체의 item
field내부를 받아낸 것으로 볼 수 있다.
https://fastapi.tiangolo.com/tutorial/body-fields/
path, query parameter에서도 그랬듯이 request body에서도 validation이 가능하다. 단, request body의 경우는 여러 field들로 이루어져 있기 때문에 각각의 validation이 필요하다. 따라서 pydantic의 Field
라는 것이 필요하다. 오해하지 말아야할 것이 Field
는 Query
, Path
, Body
와 같이 fastapi
모듈이 아니라 pydantic
의 모듈이다. 따라서, pydantic에서 직접 import해야한다.
from typing import Annotated, Union
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = Field(
default=None,
title="The description of the item",
max_length=300
)
price: float = Field(
gt=0,
description="The price must be greater then zero"
)
tax: Union[float, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
results = {"item_id": item_id, "item": item}
return results
각 model의 field마다 Field
를 적용해서 metadata와 validation을 추가할 수 있다. validation이나 metadata를 보면 Query
, Path
, Body
와 별반 다를바 없다.
위와 같이 Field
를 value처럼 대입문을 통해 줄 수도 있지만 Annotated
를 사용하는 방법도 있다.
class Item(BaseModel):
name: str
description: Annotated[Union[str, None], Field(
title="The description of the item",
max_length=300
)] = None
price: Annotated[float, Field(
gt=0,
description="The price must be greater then zero"
)]
tax: Union[float, None] = None
둘 다 똑같은 결과를 가져오니 상관없다.
https://fastapi.tiangolo.com/tutorial/body-nested-models/
fastapi에서는 pydantic덕분에 nested model을 깊이 정의하고 유효성 검사를 하고, 문서화할 수 있다.
list
타입을 만들 때 python3.9미만의 경우는 typing
을 통해 List
를 가져와 써야했다.
from typing import List
my_list: List[str]
python3.9이상부터는 list
를 바로 사용해서 쓸 수 있다. 우리는 python3.9를 쓰고 있으므로 list
로 사용하면 된다.
my_list: list[str]
따라서 다음과 같이 list를 받는 모델을 만들 수 있다.
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: list[str] = []
tags
는 str
을 element로 가지는 list
인 것이다. 이제 ["hello", "good", "afternoon"]
을 tags
로 받아낼 수 있다.
이는 list뿐만 아니라 set도 마찬가지인데, python3.9미만에서 set은 Set
type을 typing
을 통해서 가져와야 했다.
from typing import Set
my_list: Set[str]
python3.9이상부터는 다음과 같이 할 수 있다.
my_list: set[str]
str
을 element로 받아내는 list
를 만든 것이다.
pydantic
을 상속받은 class model은 하나의 type이 되어 또 다른 class model에 field로 사용될 수 있다. 가령 client가 다음의 json data를 request body로 전달한다고 하자.
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2,
"tags": ["rock", "metal", "bar"],
"image": {
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
}
}
image
부분을 잘 보면 nested로 json이 하나 더 있는 것으로 보인다. 이 부분에 대해서 pydantic class model을 하나 만들고 image
field의 type으로 넣어줄 수 있을 것 같아 보인다.
from typing import Union
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
app = FastAPI()
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: set[str] = set()
image: Union[Image, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
위의 Item
의 imgae
field type을 보면 Image
가 있는 것을 알 수 있다. Image
class model은 pydantic
의 BaseModel
을 상속받아, pydantic의 검사 대상이 될 수 있다. 따라서 pydantic이 알아서 data 유효성 검사도 해주고, data type conversion도 해주고, 문서화도 도와주는 것이다.
curl -X PUT -H "Content-Type: application/json" "localhost:8888/items/12" -d '{"name":"Foo","description":"The pretender","price":42,"tax":3.2,"tags":["rock","metal","bar"],"image":{"url":"http://example.com/baz.jpg","name":"The Foo live"}}'
{"item_id":12,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2,"tags":["rock","bar","metal"],"image":{"url":"http://example.com/baz.jpg","name":"The Foo live"}}}
문제없이 구동되는 것을 볼 수 있다.
pydantic은 python의 str
, int
, float
이 외의 추가적인 type들을 제공해주는데, 대표적으로 HttpUrl
같은 것이 있다. 더 많은 정보는 다음의 링크를 참고하자. https://docs.pydantic.dev/latest/concepts/types/
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
url
은 이제 유효한 URL인지 검사된다.