여러 개의 Body 파라미터

여러 개의 Body 파라미터 (Body - Multiple Parameters)

지금까지 PathQuery를 써 봤으니, 이제 요청 body를 선언하는 좀 더 고급진 방법을 볼 차례예요. 하나의 함수에서 경로 파라미터와 쿼리 파라미터, 그리고 body 파라미터를 얼마든지 자유롭게 섞을 수 있고, body 파라미터도 여러 개를 동시에 선언할 수 있어요.

출처: 공식문서

Path, Query, body 파라미터 섞기 (Mix Path, Query and body parameters)

당연한 얘기지만 Path, Query, 그리고 요청 body 파라미터는 자유롭게 섞어 선언할 수 있고, FastAPI가 알아서 처리해요.

body 파라미터는 기본값을 None으로 주면 선택적으로 만들 수 있어요:

from typing import Annotated

from fastapi import FastAPI, Path
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: 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: str | None = None,
    item: Item | None = None,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if item:
        results.update({"item": item})
    return results

파이썬 3.10+ - Annotated를 안 쓰는 버전: 가능하면 Annotated 버전을 쓰는 걸 권해요.

from fastapi import FastAPI, Path
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


@app.put("/items/{item_id}")
async def update_item(
    *,
    item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
    q: str | None = None,
    item: Item | None = None,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if item:
        results.update({"item": item})
    return results

여기서 눈여겨볼 점이 하나 있어요. 이 경우 body에서 가져오는 itemNone 기본값을 가지니까 선택적이에요.

여러 개의 body 파라미터 (Multiple body parameters)

앞선 예시에서는 path operation이 Item의 속성들로 이루어진 JSON body를 기대했죠:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

하지만 body 파라미터를 여러 개, 예를 들어 itemuser를 선언할 수도 있어요:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: 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

이 경우 FastAPI는 함수에 body 파라미터가 두 개(Pydantic 모델이 두 개) 있다는 걸 알아채요.

그러면 파라미터 이름을 body 안의 키(필드명)로 사용하고, 이런 body를 기대하게 돼요:

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    }
}

즉, 여러 개의 body 파라미터가 있으면 FastAPI는 각각을 자기 이름을 키로 한 단일 body 안의 필드로 간주해요.

body에 단일 값을 넣는 Body() (A single body value)

여러 개의 body 파라미터를 선언하면서, 굳이 모델이 아니라 단일 값(스칼라)을 body로 받고 싶을 때가 있어요. 그럴 땐 Body()를 쓰면 돼요. 예를 들어 importance처럼 정수 하나를 추가로 받고 싶다면:

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: str | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User, importance: int = Body()):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results

이러면 body가 이렇게 돼요:

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    },
    "importance": 5
}

itemuser는 Pydantic 모델이니 자기 이름을 키로 쓰고, 단일 값 importance는 그냥 자기 이름 값으로 들어가요.

Query 파라미터와 기본 키워드 (*) (Query parameter and the keyword *)

body 파라미터와 쿼리 파라미터를 섞을 때 순서 때문에 고민될 수 있죠. FastAPI는 어떤 게 쿼리인지 body인지 알아서 구분해요. 다만 body 파라미터 뒤에 단순(기본값 없는) 파라미터를 선언해야 할 때는, 명시적으로 * 키워드를 써서 그 뒤의 인자가 모두 키워드 전용(keyword-only)임을 나타내야 해요:

from typing import Annotated

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: str | None = None


@app.put("/items/{item_id}")
async def update_item(
    *,
    item_id: int,
    item: Item,
    user: User,
    importance: Annotated[int, Body(gt=0)],
    q: str | None = None,
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    if q:
        results.update({"q": q})
    return results

여기서:

  • item_id는 경로 파라미터
  • item, user는 body 파라미터
  • importancegt=0 검증이 붙은 body 파라미터
  • q는 쿼리 파라미터

FastAPI는 이들을 자동으로 올바른 위치에서 가져와요. importance: Annotated[int, Body(gt=0)]처럼 Body() 안에 검증 조건을 넣을 수도 있죠.

단일 body 파라미터 임베드하기 (Embed a single body parameter)

이제 재미있는 부분이에요. 단 하나의 Pydantic 모델 파라미터만 body로 선언하면, FastAPI는 그 모델을 통째로 body에 직접 넣을 거라고 기대해요. 그런데 만약 "이 필드는 item이라는 키 안에 들어있어야 한다"처럼 키로 감싸서 받고 싶다면, Body(embed=True)를 쓰면 돼요:

from typing import Annotated

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: 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

embed=True를 쓰면 body를 이렇게 받아요:

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}

item이라는 키로 감싸진 형태죠. embed=True를 빼면 이전처럼 Item의 필드가 body 최상위에 그대로 오는 형태를 기대해요. 이 둘의 차이를 인지하고 상황에 맞게 쓰면 돼요.

더 알아보기 (Learn more)