중첩 모델

중첩 모델 (Body - Nested Models)

모델의 필드 타입이 또 다른 Pydantic 모델일 수 있다는 건 아주 강력한 기능이에요. FastAPI는 Pydantic 덕분에 얼마든지 깊게 중첩된 모델을 정의하고, 검증하고, 문서화하고, 사용할 수 있어요. JSON의 "객체 안의 객체 안의 객체" 같은 구조를 자연스럽게 다룰 수 있죠.

출처: 공식문서

리스트 필드 (List fields)

모델의 속성을 하위 타입으로 선언할 수 있어요. 예를 들어 Python list:

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
    tags: list = []


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

이렇게 하면 tags는 리스트가 돼요. 다만 여기선 리스트 요소의 타입은 선언하지 않았어요.

타입 파라미터가 있는 리스트 (List fields with type parameter)

그런데 Python에는 내부 타입("타입 파라미터")을 가진 리스트를 선언하는 특별한 방법이 있어요.

타입 파라미터를 가진 list 선언하기

list, dict, tuple처럼 타입 파라미터(내부 타입)가 있는 타입을 선언하려면, 대괄호 [, ] 안에 내부 타입을 넣으면 돼요:

my_list: list[str]

이건 그냥 표준 Python 타입 선언 문법이에요. 모델 속성에도 똑같이 쓰면 되죠.

그래서 우리 예시에서 tags를 구체적으로 "문자열의 리스트"로 만들 수 있어요:

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
    tags: list[str] = []


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

이제 tags는 "문자열 리스트"로 검증·문서화돼요.

셋 타입 (Set types)

잠깐 생각해 보면, 태그(tags)는 중복되면 안 되겠죠. 아마 유일한 문자열들이어야 할 거예요.

파이썬에는 유일한 항목들의 집합을 위한 특별한 타입 set이 있어요. tags를 문자열 셋으로 선언해 볼게요:

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
    tags: set[str] = set()


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

이렇게 하면 중복된 데이터가 담긴 요청을 받아도 유일한 항목들의 set으로 변환돼요. 응답으로 내보낼 때도 원본에 중복이 있었어도 유일한 항목들의 set으로 나가요. 문서에도 set으로 적절히 표기되고요.

중첩 모델 (Nested Models)

Pydantic 모델의 각 속성은 타입을 가져요. 그런데 그 타입이 또 다른 Pydantic 모델일 수 있어요.

그러면 깊게 중첩된 JSON "객체"를, 정확한 속성 이름과 타입, 검증까지 붙여서 선언할 수 있게 돼요. 그것도 얼마든지 중첩해서요.

하위 모델 정의하기

예를 들어 Image 모델을 정의해 볼게요:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


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

Itemimage 필드가 이제 Image 모델이에요. Python 문법 그대로 사용하니까, FastAPI가 요청 body에서 해당 구조를 기대하고 검증해요. imageNone 기본값이 있으니 선택적이에요.

이제 요청 body는 이렇게 생겼겠죠:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/foo.jpg",
        "name": "Foo"
    }
}

특별한 타입과 검증 (Special types and validation)

일반 str 대신 Pydantic이 제공하는 특별한 타입도 쓸 수 있어요. 예를 들어 URL을 위한 HttpUrl이 있어요. 이건 문자열이지만 URL 형식 검증이 붙어요:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


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

이제 image.url은 유효한 URL이 아니면 검증 오류가 나요.

하위 모델의 리스트 (List of submodels)

이제 한 걸음 더 나아가서, Image 모델의 리스트를 필드로 가질 수도 있어요:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[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

imagesImage 객체들의 리스트(선택적, None 가능)가 돼요. body는 이렇게 오겠죠:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "images": [
        {
            "url": "http://example.com/foo.jpg",
            "name": "Foo"
        },
        {
            "url": "http://example.com/bar.jpg",
            "name": "Bar"
        }
    ]
}

깊게 중첩된 모델 (Deeply nested models)

얼마든지 깊게 중첩할 수 있어요. Offer라는 모델이 Item의 리스트를 가지고, ItemImage의 리스트를 가지는 식으로요:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


class Offer(BaseModel):
    name: str
    description: str | None = None
    price: float
    items: list[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

중첩이 깊어져도 Pydantic과 FastAPI가 재귀적으로 다 검증하고 처리해요. OfferitemsItem들의 리스트, 그 안의 imagesImage들의 리스트가 되는 식이죠.

순수 리스트 형태의 body (Bodies of pure lists)

지금까지는 모델 안의 필드로 리스트를 봤는데, body 자체가 순수한 리스트일 수도 있어요. 바로 images: list[Image]처럼 파라미터 타입만으로 선언하면 돼요:

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images

이제 클라이언트는 이미지 객체들의 리스트를 body로 보내야 해요.

임의의 dict body (Bodies with arbitrary dicts)

마지막으로, 모델 없이 dict로 body를 선언할 수도 있어요. 이때 키와 값의 타입을 지정할 수 있어요. 아래처럼 정수 키에 실수 값을 갖는 dict를 body로 받아요:

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
    return weights

dict[int, float]는 정수 키와 실수 값으로 구성된 dict를 의미해요. FastAPI가 이 구조대로 파싱하고 검증해요. 같은 형식이 쿼리 파라미터에도 적용될 수 있어요.

더 알아보기 (Learn more)