중첩 모델
중첩 모델 (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
Item의 image 필드가 이제 Image 모델이에요. Python 문법 그대로 사용하니까, FastAPI가 요청 body에서 해당 구조를 기대하고 검증해요. image는 None 기본값이 있으니 선택적이에요.
이제 요청 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
images는 Image 객체들의 리스트(선택적, 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의 리스트를 가지고, Item은 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
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가 재귀적으로 다 검증하고 처리해요. Offer의 items는 Item들의 리스트, 그 안의 images는 Image들의 리스트가 되는 식이죠.
순수 리스트 형태의 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가 이 구조대로 파싱하고 검증해요. 같은 형식이 쿼리 파라미터에도 적용될 수 있어요.