FastAPI를 배워보자 6일차 - Body - Multiple parameter, Fields, Nested Models

0

fastapi

목록 보기
6/13

Body - Multiple parameter, Fields, Nested Models

Body - Multiple parameter

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로 itemuser가 있는 것이다. 단편적으로 떠오르는 생각은 하나의 JSON에 itemuser를 받아야하므로 BaseModel을 상속받은 python class를 itemuser 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은 itemuser을 각각 받아낼 수 있을 것이다.

그러나 위와 같이 사용하지 않고 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는 itemuser 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를 만들 필요가 없다.

Singular valies in body

request body도 singular body로 받아낼 수 있다. 즉 int, float, str 등으로 받아낼 수 있다는 것이다. 다만, 그대로 쓸 수는 없는데, 왜냐하면 query parameter와 헷갈릴 수 있기 때문이다. 따라서 Body객체를 이용해서 해당 parameter가 request body라는 것을 지시해주어야 한다.

가령, importance라는 body parameter를 쓰고 싶은데 int타입이라서 BaseModel를 상속받은 class model까지 쓰고 싶지않다면 Bodyimportance라는 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임을 명백하게 알려주는 것이 필요하다.

Embed a single 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를 만드는 것이 최선일 것이다.

그러나, 실제적으로 원하는 것은 ItemModelitem field 내부의 값들이다. 이 값들을 얻기위해서 ItemModel을 따로 만드는 것 자체도 굉장히 귀찮은 일이다. fastapi에서는 이를 위해서 Embed옵션을 제공해주는데, Embed는 안에 있는 field를 가져와주는 기능을 한다. Body(embed=True)로 하면되고, EmbedTrue가 되면, 오직 하나의 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내부를 받아낸 것으로 볼 수 있다.

Body - Fields

https://fastapi.tiangolo.com/tutorial/body-fields/

path, query parameter에서도 그랬듯이 request body에서도 validation이 가능하다. 단, request body의 경우는 여러 field들로 이루어져 있기 때문에 각각의 validation이 필요하다. 따라서 pydantic의 Field라는 것이 필요하다. 오해하지 말아야할 것이 FieldQuery, 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

둘 다 똑같은 결과를 가져오니 상관없다.

Body - Nested Models

https://fastapi.tiangolo.com/tutorial/body-nested-models/

fastapi에서는 pydantic덕분에 nested model을 깊이 정의하고 유효성 검사를 하고, 문서화할 수 있다.

List fields, Set fields

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] = []

tagsstr을 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를 만든 것이다.

Nested Models

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

위의 Itemimgae field type을 보면 Image가 있는 것을 알 수 있다. Image class model은 pydanticBaseModel을 상속받아, 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"}}}

문제없이 구동되는 것을 볼 수 있다.

Special types and validation

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인지 검사된다.

0개의 댓글

관련 채용 정보