Шаг 04. Pydantic v2 — схемы валидации и сериализации
⚡ Кратко: что делаем на этом шаге
Цель: Написать Pydantic v2 схемы для валидации входа (Create) и сериализации выхода (Out). Подключить model_config = ConfigDict(from_attributes=True) для чтения ORM-объектов.
- Файлы:
app/schemas/question.py,app/schemas/response.py,app/schemas/__init__.py - Ключевые классы:
QuestionCreate,QuestionOut,ResponseCreate,ResponseOut - Результат: входящий JSON валидируется, ORM-объекты сериализуются без ручного
.to_dict()
🎯 Цель этапа
Pydantic v2 играет две роли в нашем API: валидация входа (проверяем JSON из запроса)
и сериализация выхода (преобразуем ORM-объекты в JSON-совместимые словари).
Разделяем эти роли в отдельные классы: XxxCreate для входа, XxxOut для выхода.
ConfigDict(from_attributes=True) (бывший orm_mode = True в v1)
позволяет Pydantic читать атрибуты Python-объектов — в том числе SQLAlchemy моделей.
Тогда QuestionOut.model_validate(question_orm_obj) работает напрямую.
После этого шага у нас будет
- Схемы для
Question:QuestionCreate(валидация) иQuestionOut(сериализация) - Схемы для
Response:ResponseCreateиResponseOut - Общая схема
ErrorOutдля единообразных ответов с ошибками
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
app/schemas/question.py | Создать | QuestionCreate, QuestionOut |
app/schemas/response.py | Создать | ResponseCreate, ResponseOut |
app/schemas/common.py | Создать | Общие схемы: ErrorOut, MessageOut |
app/schemas/__init__.py | Изменить | Реэкспорт всех схем |
🔨 Шаги
1. Схемы для Question
# app/schemas/question.py
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class QuestionCreate(BaseModel):
"""
Схема входных данных для создания/обновления вопроса.
Используется в POST /questions/ и PUT /questions/<id>.
Pydantic валидирует данные из request.get_json() перед записью в БД.
"""
text: str = Field(
..., # обязательное поле (no default)
min_length=5,
max_length=500,
description="Текст вопроса",
examples=["Согласны ли вы с удалённой работой?"],
)
class QuestionOut(BaseModel):
"""
Схема выходных данных вопроса — то, что возвращает API.
model_config с from_attributes=True позволяет создавать из ORM-объектов:
question_out = QuestionOut.model_validate(question_orm_object)
"""
model_config = ConfigDict(from_attributes=True)
id: int
text: str
created_at: datetime
class QuestionListOut(BaseModel):
"""Схема для списка вопросов с пагинационной информацией."""
model_config = ConfigDict(from_attributes=True)
items: list[QuestionOut]
total: int
2. Схемы для Response
# app/schemas/response.py
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class ResponseCreate(BaseModel):
"""
Схема для добавления голоса.
Используется в POST /questions/<id>/responses/.
question_id не нужен — берётся из URL-параметра.
"""
is_agree: bool = Field(
...,
description="True = согласен, False = не согласен",
)
class ResponseOut(BaseModel):
"""Схема выходных данных голоса."""
model_config = ConfigDict(from_attributes=True)
id: int
question_id: int
is_agree: bool
created_at: datetime
3. Общие схемы ошибок
# app/schemas/common.py
from pydantic import BaseModel
class ErrorOut(BaseModel):
"""Единообразный формат ошибок API."""
error: str
detail: str | None = None
class MessageOut(BaseModel):
"""Простое сообщение об успехе."""
message: str
4. Экспортируем все схемы
# app/schemas/__init__.py
from app.schemas.question import QuestionCreate, QuestionOut, QuestionListOut
from app.schemas.response import ResponseCreate, ResponseOut
from app.schemas.common import ErrorOut, MessageOut
__all__ = [
"QuestionCreate",
"QuestionOut",
"QuestionListOut",
"ResponseCreate",
"ResponseOut",
"ErrorOut",
"MessageOut",
]
🧠 Объяснение логики
Почему Create и Out — разные классы
На входе и выходе данные разные. При создании вопроса клиент присылает только text.
В ответе мы возвращаем id, text, created_at.
Один класс для обоих случаев либо заставит добавлять Optional везде,
либо рискует вернуть лишнее (например, хеш пароля в схеме пользователя).
model_validate() vs dict()
Паттерн использования в эндпоинтах (покажем в шаге 05):
# Валидация входа:
data = request.get_json()
schema = QuestionCreate.model_validate(data) # бросает ValidationError если данные неверны
# Сериализация выхода:
out = QuestionOut.model_validate(question) # читает атрибуты ORM-объекта
return jsonify(out.model_dump()), 200
Pydantic v1 vs v2
| Pydantic v1 (уроки 09–11) | Pydantic v2 (капстоун) |
|---|---|
class Config: orm_mode = True |
model_config = ConfigDict(from_attributes=True) |
Schema(**data) |
Schema.model_validate(data) |
schema.dict() |
schema.model_dump() |
schema.json() |
schema.model_dump_json() |
class Config: orm_mode = True)
работает с предупреждением, но будет удалён. Используйте только ConfigDict.
✅ Проверка
1. Тестируем схемы в flask shell
flask shell
from app.schemas import QuestionCreate, QuestionOut
from pydantic import ValidationError
# Валидный вопрос
q = QuestionCreate(text="Согласны ли вы с удалённой работой?")
print(q.text) # Согласны ли вы с удалённой работой?
# Слишком короткий — должна быть ValidationError
try:
bad = QuestionCreate(text="Hi")
except ValidationError as e:
print(e) # 1 validation error for QuestionCreate ...
# Сериализация из словаря
q_out = QuestionOut.model_validate({
"id": 1, "text": "Тест", "created_at": "2025-01-01T00:00:00"
})
print(q_out.model_dump())
exit()
2. Ожидаемый результат
QuestionCreate проходит валидацию для длинных строк и бросает ошибку для коротких.
QuestionOut.model_dump() возвращает словарь с нужными полями.
➡️ Что дальше
На следующем шаге создаём Blueprints и CRUD-эндпоинты. Схемы из этого шага
будут использоваться в каждом эндпоинте: QuestionCreate для валидации
тела запроса, QuestionOut для формирования ответа.
- Готово: схемы QuestionCreate/Out, ResponseCreate/Out, ErrorOut — все протестированы
- Далее (шаг 05): blueprints, CRUD-эндпоинты, фильтрация через request.args