중첩 모델

중첩 모델로 복잡한 본문 다루기 (Body - Nested Models)

FastAPI 덕분에 우리는 깊고 깊게 중첩된 모델을 얼마든지 정의하고, 검증하고, 문서화하고, 직접 사용할 수 있어요. 전부 Pydantic 덕분이죠. list, set, dict 같은 파이썬 표준 타입에 내부 타입을 담는 문법만 알면 반복 구조가 복잡한 JSON 본문도 쉽게 표현해요.

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

본문

리스트 필드

모델 속성에 리스트 타입을 하나 붙여 볼게요. 그냥 list라고만 선언하면 요소의 타입은 정해지지 않아요.

from fastapi import FastAPI
from pydantic import BaseModel


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


app = FastAPI()


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

타입 파라미터와 함께 쓰는 리스트 필드

요소의 타입까지 정하고 싶다면 대괄호 [ ] 안에 내부 타입을 "타입 파라미터"로 넣어요. list[str]처럼요. 이건 그냥 표준 파이썬 문법이에요.

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

집합(Set) 타입

태그는 중복되지 않아야 하니까 중복을 허용하지 않는 set으로 바꿔 볼게요. 요청에 중복된 데이터가 와도 자동으로 고유한 값들의 집합으로 변환돼요. 데이터를 내보낼 때도 마찬가지로 중복 없이 나가요.

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

중첩 모델

Pydantic 모델의 각 속성은 타입을 하나씩 갖는데, 그 타입이 또 다른 Pydantic 모델일 수 있어요. 그래서 이름과 타입과 검증 규칙을 갖춘 JSON 객체를 아주 깊게 중첩해서 선언할 수 있죠. 아래처럼 Image 모델을 정의해 볼게요.

from fastapi import FastAPI
from pydantic import BaseModel


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 = FastAPI()


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

image 속성에 Image를 타입으로 쓰면 FastAPI는 이런 본문을 기대해요.

{
    "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"
    }
}

선언만 해도 중첩 모델까지 에디터 지원, 데이터 변환, 검증, 자동 문서화를 그대로 받을 수 있어요.

특별한 타입과 검증

str을 상속받는 더 복잡한 타입도 쓸 수 있어요. 예를 들어 Imageurl 필드를 평범한 str 대신 Pydantic의 HttpUrl 인스턴스로 선언하면, 값이 꼭 유효한 URL이어야 하고 문서에도 그렇게 표시돼요.

from pydantic import BaseModel, HttpUrl


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

서브모델의 리스트

Pydantic 모델을 또 listset의 내부 타입으로도 쓸 수 있어요. images 필드에 Image들의 리스트를 담으면, 요청 본문이 "이미지 객체들의 배열"을 받게 돼요.

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

깊게 중첩된 모델

이런 조합이면 마음껏 깊게 중첩할 수 있어요. 예를 들어 OfferItem들의 리스트를, 각 Item이 또 옵션으로 Image들의 리스트를 갖는 구조도 가능하죠.

순수 리스트 본문

최상위 값이 JSON 배열이라면 함수 파라미터 타입을 list[Image]처럼 선언해 주면 돼요. 그래도 에디터 지원과 변환, 검증을 그대로 받을 수 있어요.

임의의 dict 본문

키와 값을 다른 타입으로 갖는 dict로도 선언할 수 있어요. 이러면 Pydantic 모델처럼 필드 이름을 미리 알지 못해도 임의의 키를 받아들일 수 있죠. 예를 들어 int 키에 float 값을 요구하는 dict[int, float]로 선언하면, 그 형식만 맞으면 아무 dict나 받아요.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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

참고로 JSON은 키로 문자열만 지원하지만, Pydantic이 자동 변환을 해 주기 때문에 클라이언트가 "순수한 정수"로만 된 문자열 키를 보내면 정수 키로 바꿔 검증·변환해 줘요. 결국 weights는 실제로 int 키와 float 값만 갖는 dict가 돼요.

더 알아보기