Вложенные модели, списки и опциональные поля
Pydantic валидирует данные любой вложенности: модель может содержать другие модели, списки объектов, словари и опциональные поля — проверка идёт рекурсивно.
Реальные данные редко плоские. Заказ содержит список позиций, у пользователя есть адрес-объект, у товара — теги-список. Pydantic проверяет такие структуры на любую глубину автоматически.
До сих пор поля моделей были простыми: строки, числа, булевы. Но тело реального запроса — это дерево: объект внутри объекта, массив объектов, опциональные ветки. Pydantic справляется, потому что тип поля может быть другой моделью или контейнером моделей. Проверка тогда выполняется рекурсивно: чтобы провалидировать заказ, надо провалидировать каждую его позицию.
from pydantic import BaseModel
class Address(BaseModel):
city: str
zip_code: str
class OrderItem(BaseModel):
product_id: int
quantity: int = 1
class Order(BaseModel):
customer: str
address: Address # вложенная модель
items: list[OrderItem] # список моделей
comment: str | None = None # опциональное поле
Запрос с таким телом FastAPI разберёт целиком: проверит customer, рекурсивно — address как Address, каждый элемент items как OrderItem, а comment допустит отсутствующим. Ошибка в любой ветке (например, отрицательное quantity, если бы стояло ограничение) вернётся с точным указанием пути до проблемного поля.
Как работает под капотом
Рекурсивная валидация — это обход дерева: для каждого поля смотрим тип; если это модель — спускаемся внутрь, если список моделей — проверяем каждый элемент. Смоделируем рекурсивный валидатор на stdlib:
def validate(value, spec, path="root"):
errors = []
if isinstance(spec, dict): # вложенный объект
if not isinstance(value, dict):
return [f"{path}: ожидался объект"]
for key, subspec in spec.items():
if key not in value:
errors.append(f"{path}.{key}: поле отсутствует")
else:
errors += validate(value[key], subspec, f"{path}.{key}")
elif isinstance(spec, list): # список объектов
for i, item in enumerate(value):
errors += validate(item, spec[0], f"{path}[{i}]")
else: # простой тип
if not isinstance(value, spec):
errors.append(f"{path}: ожидался {spec.__name__}")
return errors
order_spec = {
"customer": str,
"address": {"city": str, "zip_code": str},
"items": [{"product_id": int, "quantity": int}],
}
good = {"customer": "Аня", "address": {"city": "Москва", "zip_code": "101000"},
"items": [{"product_id": 1, "quantity": 2}]}
bad = {"customer": "Боб", "address": {"city": "Казань"},
"items": [{"product_id": "x", "quantity": 1}]}
print("good:", validate(good, order_spec))
print("bad :", validate(bad, order_spec))
Попробуй сам ▶ Видно, что ошибки указывают точный путь: root.address.zip_code, root.items[0].product_id — так же подробно сообщает Pydantic.
Частые ошибки
Первая — описывать вложенный объект как dict вместо отдельной модели, теряя валидацию его внутренностей. Вторая — путать list[Item] (список объектов) и Item (один объект). Третья — делать поле опциональным через str | None, но забывать дефолт = None, из-за чего оно остаётся обязательным (тип допускает None, но значение всё равно требуется прислать). Четвёртая — чрезмерная вложенность; иногда структуру стоит уплостить ради простоты API.
Best practices
- Каждую вложенную сущность описывайте отдельной моделью, а не
dict. - Списки объектов —
list[Model]; FastAPI провалидирует каждый элемент. - Для опционального поля указывайте и тип
| None, и значение по умолчанию= None. - Ошибки валидации содержат путь до поля — используйте его при отладке.
Рекурсивные и обобщённые структуры
Вложенность может быть не только конечной, но и рекурсивной: дерево комментариев, где у комментария есть список ответов того же типа. Pydantic поддерживает такие самоссылающиеся модели — поле объявляют типом самой модели в строковой форме или через отложенные аннотации, и валидация корректно обходит дерево любой глубины. Другой мощный приём — обобщённые модели-обёртки: единый формат ответа Page[T] с полями items, total, page, где T — тип элемента. Так вы описываете пагинацию один раз и переиспользуете для любого ресурса. Эти возможности показывают, что система типов Pydantic — не игрушечная: она выражает реальные структуры данных, от деревьев до обобщённых контейнеров, и проверяет их так же строго, как простые поля, всегда указывая точный путь до проблемы при ошибке.
Итог: Pydantic валидирует структуры любой вложенности рекурсивно, проверяя каждую модель и каждый элемент списка, и сообщает точный путь до ошибочного поля. Описывайте вложенные сущности отдельными моделями.