$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогКак устроена валидация данных: от Pydantic до ошибок для пользователя

Как устроена валидация данных: от Pydantic до ошибок для пользователя

$ sudo teach IT
·21 июля 2026 г.·10 мин·55
Как устроена валидация данных: от Pydantic до ошибок для пользователя

Покажем, как формируется набор ошибок, как корректно возвращать их клиенту и как сделать сообщения понятными, а не “шумными”.

Содержание
1. От Pydantic до уровня API: где именно рождаются ошибки1.1. Pydantic как фабрика структуры ошибок1.2. Ошибка как исключение: смысл ValidationError1.3. Разделение типов проблем: технические и бизнес-ошибки2. Как формируется набор ошибок: anatomy ошибок Pydantic2.1. Типичная структура данных ошибки2.2. Почему “loc” — это и сила, и источник проблем3. Контракт ошибок: как вернуть их клиенту без лишнего шума3.1. Важный принцип: отделяйте “машинную” и “человеческую” части3.2. Где именно перехватывать: чтобы не ломать поведение FastAPI4. Пример: пользовательские ошибки для валидации в FastAPI4.1. Модель и ручные ограничения4.2. Функции нормализации: делаем loc дружелюбным4.3. Нормализация сообщений: убираем “шум”4.4. Обработчик ошибок и формирование контракта4.5. Проверка: что увидит клиент5. Самые частые ошибки при работе с валидацией и сообщениями5.1. Отдавать “loc” как есть, без нормализации5.2. Смешивать в одном ответе ошибки валидации и бизнес-ошибки, не различая тип5.3. Игнорировать локализацию5.4. Не думать про стабильность формата ошибок5.5. Делать “слишком умные” сообщения и терять причинность6. Как сделать сообщения понятными: практические стратегии6.1. Уровень “для пользователя”: кратко, предсказуемо, без технических формулировок6.2. Уровень “для разработчика”: детали можно оставить в логах или в расширенном поле6.3. Единый стиль: одна терминология для всех ручных валидаторов7. Типовые расширения: кастомные сообщения и карта ошибок для фронтенда7.1. Локализация через type вместо парсинга текста7.2. Тестирование контракта ошибок8. Вывод: валидация — это UX, а не просто “422”

Валидация данных — это не только про «проверить типы». В нормальном приложении она превращается в управляемую подсистему: формируется понятный набор ошибок, эти ошибки безопасно и корректно возвращаются клиенту, а сообщения превращаются из “шумного технического лога” в удобный интерфейс для пользователя (и одновременно — в достаточно подробную диагностику для разработчика).

В экосистеме Python одну из самых удобных основ под валидацию даёт Pydantic (в том числе через интеграции с FastAPI). Но даже при использовании готовых библиотек многие команды сталкиваются с одними и теми же проблемами:

  • ошибки приходят в виде “массивов” без ясного смысла для человека;
  • клиенту возвращаются слишком детали/внутренняя структура модели;
  • сообщения дублируются, путаются по полям или не соответствуют ожидаемому формату фронтенда;
  • часть ошибок формируется не в том месте (например, валидация тела запроса — отдельно, а бизнес-валидация — отдельно), и в итоге клиент получает несогласованный интерфейс.

Ниже разберём, как на самом деле устроен путь ошибки: от стадии формирования “карты” ошибок в Pydantic до финального ответа API. А ещё — как сделать так, чтобы клиент получил человеческие сообщения, не теряя при этом точности.


1. От Pydantic до уровня API: где именно рождаются ошибки

1.1. Pydantic как фабрика структуры ошибок

Pydantic валидирует входные данные (обычно dict или объект), пытаясь сопоставить их структуре модели. Когда что-то не сходится — формируется список проблем (обычно это errors() / model_validate-механизмы внутри Pydantic).

С точки зрения приложения это важно: валидатор обычно не “возвращает сообщение”, а создаёт структурированную информацию:

  • какое поле/путь не прошло валидацию,
  • какой тип ошибки,
  • параметры (например, ожидаемые длины, паттерн, значение и т. п.),
  • итоговое человекочитаемое описание.

На практике, если вы поймёте эту структуру, вы сможете:

  1. аккуратно преобразовывать её под формат вашего API;
  2. локализовать/нормализовать сообщения;
  3. выводить клиенту ровно то, что нужно, без лишней “технической пены”.

1.2. Ошибка как исключение: смысл ValidationError

Когда валидация не проходит, Pydantic возбуждает исключение (типично ValidationError). В FastAPI оно перехватывается интеграционным слоем и превращается в HTTP-ответ (как правило, 422 Unprocessable Entity).

И ключевой момент: в этом этапе ошибки уже имеют достаточно точную структуру — осталось правильно её интерпретировать и, при необходимости, перепаковать.

1.3. Разделение типов проблем: технические и бизнес-ошибки

Важно различать:

  • ошибки валидации схемы (не проходит тип/формат/ограничения полей);
  • ошибки бизнес-логики (например, “email уже зарегистрирован”, “дата окончания раньше даты начала”);
  • ошибки доступа/авторизации (тут другая подсистема);
  • ошибки инфраструктуры (таймауты, падения внешних сервисов).

Если смешивать их в один массив без контекста — клиент быстро теряет смысл происходящего. Поэтому даже если источник ошибок — Pydantic, вам нужно продумать общий контракт ошибок для API.


2. Как формируется набор ошибок: anatomy ошибок Pydantic

2.1. Типичная структура данных ошибки

В Pydantic (и в интеграциях с FastAPI) вы почти всегда встретите концепцию “ошибка на пути”. Часто путь оформляется как список сегментов (например, ["items", 0, "price"]).

Логика такая:

  • loc — локация (путь до проблемного места);
  • msg — сообщение;
  • type — класс/категория ошибки (например, value_error, type_error, missing, и т. п.);
  • дополнительные параметры (в зависимости от категории).

Внутри ValidationError это можно увидеть так:

code
from pydantic import BaseModel, ValidationError, Field
from typing import List

class Item(BaseModel):
    name: str
    price: float = Field(gt=0)

class Order(BaseModel):
    items: List[Item]
    quantity: int = Field(ge=1)

try:
    Order.model_validate({"items": [{"name": "Pen", "price": -10}], "quantity": 0})
except ValidationError as e:
    print(e.errors())

Результат (конкретный формат зависит от версии Pydantic), как правило будет списком словарей примерно такого вида:

  • ошибка в items.0.price (должно быть > 0),
  • ошибка в quantity (должно быть ≥ 1).

2.2. Почему “loc” — это и сила, и источник проблем

loc позволяет точно понять, где именно не сошлось. Но у клиентских приложений часто ожидается простая модель: “ошибка в поле price” или “ошибка в поле items[0].price”.

Если вы напрямую отдаёте loc как “сырые сегменты”, фронтенд может не знать, как это распаковать. Например:

  • loc = ("body", "items", 0, "price") — из FastAPI это “путь в запросе”;
  • а клиент ожидал бы field = "items[0].price" или даже более “плоское” сообщение.

Отсюда правило: внутренний путь не равен контракту API.


3. Контракт ошибок: как вернуть их клиенту без лишнего шума

3.1. Важный принцип: отделяйте “машинную” и “человеческую” части

Хороший ответ об ошибках обычно включает:

  • машинное описание (код/тип/параметры), чтобы клиент мог локализовать и сопоставлять ошибки;
  • человеческое сообщение (или набор сообщений), чтобы пользователь сразу понял, что исправить;
  • идентификацию поля (или элемента формы).

В JSON-практике популярны форматы вида:

code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Ошибка валидации данных.",
    "details": [
      {
        "field": "quantity",
        "message": "Должно быть не меньше 1",
        "type": "greater_than_equal",
        "limit": 1
      }
    ]
  }
}

Ваша задача — преобразовать структуру Pydantic/ FastAPI в такой вид.

3.2. Где именно перехватывать: чтобы не ломать поведение FastAPI

В FastAPI есть стандартные обработчики ошибок валидации. Но если вы хотите изменить формат для клиента — обычно нужно написать свой обработчик для RequestValidationError.

В базовом случае FastAPI уже отдаёт 422 с деталями. Но “детали” часто достаточно техничны: поле называется “loc”, сообщение — “msg”, плюс там присутствует лишняя информация о “body”.

Если вам нужен спокойный клиентский контракт — добавляйте обработчик и перепаковывайте.


4. Пример: пользовательские ошибки для валидации в FastAPI

Ниже пример “как сделать лучше”: мы преобразуем ошибки в предсказуемый формат, превращая loc в удобные field и нормализуя сообщения.

4.1. Модель и ручные ограничения

code
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from typing import Any, List, Dict, Optional

app = FastAPI()

class ItemIn(BaseModel):
    name: str = Field(min_length=2)
    price: float = Field(gt=0)

class CreateOrderIn(BaseModel):
    items: List[ItemIn]
    quantity: int = Field(ge=1)

4.2. Функции нормализации: делаем loc дружелюбным

Рассмотрим типичный loc, который может выглядеть как ("body", "items", 0, "price"). Для клиента лучше представить это как items[0].price (или как минимум items.0.price).

code
def loc_to_field(loc: List[Any]) -> str:
    """
    Превращаем loc из FastAPI/Pydantic в строку вида items[0].price.
    """
    # Уберём служебный префикс 'body' если он есть
    parts = [p for p in loc if p != "body"]

    result = ""
    for p in parts:
        if isinstance(p, int):
            result += f"[{p}]"
        else:
            if not result:
                result = str(p)
            else:
                result += f".{p}"
    return result or "unknown"

4.3. Нормализация сообщений: убираем “шум”

Сообщение Pydantic достаточно информативно, но часто слишком буквальное. Можно применить простую нормализацию: если msg уже понятное — оставляем, если нет — дополняем.

Важный момент: не пытайтесь “сгенерировать идеальный текст” для каждого типа ошибки. Лучше сделать 80% пользы: убрать лишние части и привести к единому стилю.

code
def normalize_message(msg: str, err_type: str, ctx: Optional[Dict[str, Any]] = None) -> str:
    # Примитивные правила; в реальном проекте лучше расширить под нужды/локаль.
    if "ensure this value is greater than or equal to" in msg:
        # Pydantic иногда формулирует очень длинно — оставим как есть или укоротим.
        return msg.replace("ensure this value is greater than or equal to", "Должно быть не меньше")
    if "ensure this value is greater than" in msg:
        return msg.replace("ensure this value is greater than", "Должно быть больше")
    if "field required" in msg.lower():
        return "Поле обязательно"
    return msg

4.4. Обработчик ошибок и формирование контракта

Перехватываем RequestValidationError (это ошибки схемы для входящих запросов).

code
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    raw_errors = exc.errors()

    details = []
    for e in raw_errors:
        loc = e.get("loc", [])
        field = loc_to_field(loc)
        msg = e.get("msg", "Validation error")
        err_type = e.get("type", "validation_error")
        ctx = e.get("ctx")  # иногда присутствует

        details.append({
            "field": field,
            "message": normalize_message(msg, err_type, ctx),
            "type": err_type,
            # ctx лучше отдавать только если вы действительно используете это на фронтенде:
            # "limit": ctx.get("limit") ...
        })

    return JSONResponse(
        status_code=422,
        content={
            "error": {
                "code": "VALIDATION_ERROR",
                "message": "Ошибка валидации данных.",
                "details": details
            }
        }
    )

4.5. Проверка: что увидит клиент

Если отправить, например:

  • quantity = 0,
  • items[0].price = -10,
  • items[0].name = "P" (слишком короткое),

клиент получит:

code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Ошибка валидации данных.",
    "details": [
      { "field": "quantity", "message": "Должно быть не меньше 1", "type": "greater_than_equal" },
      { "field": "items[0].name", "message": "String should have at least 2 characters", "type": "string_too_short" },
      { "field": "items[0].price", "message": "ensure this value is greater than 0", "type": "greater_than" }
    ]
  }
}

Здесь уже есть ключевое преимущество: клиентская форма может “подсветить” конкретные поля. А разработчик — увидеть понятную карту проблем, не анализируя сырые loc.


5. Самые частые ошибки при работе с валидацией и сообщениями

5.1. Отдавать “loc” как есть, без нормализации

loc — внутреннее представление структуры запроса. Оно меняется от контекста: иногда это ("body", "field"), иногда есть “path” или “query”. Клиенту будет больно.

Правило: преобразуйте loc к вашему стабильному формату field.

5.2. Смешивать в одном ответе ошибки валидации и бизнес-ошибки, не различая тип

Если “кол-во уже использовано” и “quantity меньше 1” будут лежать в одном массиве с одинаковыми структурами, фронтенд будет вынужден гадать.

Правило: бизнес-ошибкам присваивайте свои code/type или отдельный блок. Валидация схемы — отдельно.

5.3. Игнорировать локализацию

Сообщения Pydantic написаны на английском (по умолчанию). Если у вас русскоязычные пользователи — вам потребуется слой переводов или переформулировки.

Не обязательно переводить всё вручную: можно начать с нормализации “самых частых” типов ошибок (missing, greater_than, string_too_short, pattern mismatch). Но лучше сразу заложить механизм локализации: словарь “type → шаблон”.

5.4. Не думать про стабильность формата ошибок

Если вы один раз “подрихтовали” структуру деталей, но в следующий релиз добавили новые ключи или изменили формат field, фронтенд ломается. Стабильный контракт — часть качества продукта.

Практика: закрепляйте структуру в тестах (хотя бы интеграционных) и документируйте.

5.5. Делать “слишком умные” сообщения и терять причинность

Иногда авторы пытаются сделать “красивый” текст, но в итоге теряют параметры валидации: например, клиенту нужно не только понять “слишком короткое”, но и “минимум 2 символа”. Поэтому лучше сохранять и type, и опциональные параметры (например, limit, min_length, pattern), если это используется в UI.


6. Как сделать сообщения понятными: практические стратегии

6.1. Уровень “для пользователя”: кратко, предсказуемо, без технических формулировок

Пользователь должен видеть:

  • что именно не так (поле),
  • как исправить (минимум/максимум/формат),
  • возможно, один вариант действия (например, “введите телефон в формате …”).

6.2. Уровень “для разработчика”: детали можно оставить в логах или в расширенном поле

Хороший компромисс:

  • в ответе клиенту — минимум, который реально используется;
  • в расширенном поле (или в логах) — type, raw ctx, исходное сообщение Pydantic.

Например:

code
{
  "field": "items[0].price",
  "message": "Цена должна быть больше 0",
  "type": "greater_than",
  "debug": { "raw_msg": "ensure this value is greater than 0" }
}

Если вы не используете debug на фронтенде — уберите его или защищайте доступом.

6.3. Единый стиль: одна терминология для всех ручных валидаторов

Помимо схемы, у вас будут проверки в сервисном слое. Чтобы пользователь не видел “два разных подхода к одному и тому же”, лучше использовать единый интерфейс ошибок: тот же field/message/type.

Например, если бизнес-валидация говорит “количество не может быть меньше 1”, она должна выдавать ошибку в той же форме, что и Pydantic-ограничение ge=1.


7. Типовые расширения: кастомные сообщения и карта ошибок для фронтенда

7.1. Локализация через type вместо парсинга текста

Парсить msg строками хрупко: формулировки могут отличаться между версиями и типами валидаторов. Надёжнее:

  • брать type;
  • маппить на шаблон на нужном языке.

Пример минимальной мапы:

code
ERROR_TEMPLATES_RU = {
    "missing": "Поле обязательно",
    "string_too_short": "Слишком короткое значение (минимум {min_length} символов)",
    "greater_than": "Значение должно быть больше {limit}",
    "greater_than_equal": "Значение должно быть не меньше {limit}",
}

def render_ru_message(err_type: str, msg: str, ctx: dict | None) -> str:
    template = ERROR_TEMPLATES_RU.get(err_type)
    if not template or not ctx:
        return msg
    return template.format(**ctx)

А дальше используйте это вместо грубой заменой текста.

7.2. Тестирование контракта ошибок

Интеграционные тесты на ошибки валидации часто проще, чем кажется. Вы проверяете, что:

  • статус 422,
  • структура error.code,
  • что в details есть нужные field,
  • что сообщения содержат ожидаемые фрагменты.

Это предотвращает “тихие” поломки для фронтенда.


8. Вывод: валидация — это UX, а не просто “422”

Валидация данных в API —

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

API на FastAPI

Курс по теме

API на FastAPI

Погрузитесь в мир самого быстрого фреймворка в Python – FastAPI! Курс для тех, кто хочет освоить полноценную профессию, и писать эндпоинты на enterprise-уровне!

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API
модели

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

Разберём, как описывать доменные сущности, DTO и error-model на уровне типов, чтобы ловить несовпадения схем до продакшена. Покажем практические шаблоны для типобезопасного маппинга и версионирования контрактов.

15 сентября 2026 г.
1040
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

Поймём различия между синхронной обработкой, BackgroundTasks и внешними очередями. Разберём idempotency, ретраи и мониторинг фоновых процессов.

22 июля 2026 г.
980
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

1. От Pydantic до уровня API: где именно рождаются ошибки1.1. Pydantic как фабрика структуры ошибок1.2. Ошибка как исключение: смысл ValidationError1.3. Разделение типов проблем: технические и бизнес-ошибки2. Как формируется набор ошибок: anatomy ошибок Pydantic2.1. Типичная структура данных ошибки2.2. Почему “loc” — это и сила, и источник проблем3. Контракт ошибок: как вернуть их клиенту без лишнего шума3.1. Важный принцип: отделяйте “машинную” и “человеческую” части3.2. Где именно перехватывать: чтобы не ломать поведение FastAPI4. Пример: пользовательские ошибки для валидации в FastAPI4.1. Модель и ручные ограничения4.2. Функции нормализации: делаем loc дружелюбным4.3. Нормализация сообщений: убираем “шум”4.4. Обработчик ошибок и формирование контракта4.5. Проверка: что увидит клиент5. Самые частые ошибки при работе с валидацией и сообщениями5.1. Отдавать “loc” как есть, без нормализации5.2. Смешивать в одном ответе ошибки валидации и бизнес-ошибки, не различая тип5.3. Игнорировать локализацию5.4. Не думать про стабильность формата ошибок5.5. Делать “слишком умные” сообщения и терять причинность6. Как сделать сообщения понятными: практические стратегии6.1. Уровень “для пользователя”: кратко, предсказуемо, без технических формулировок6.2. Уровень “для разработчика”: детали можно оставить в логах или в расширенном поле6.3. Единый стиль: одна терминология для всех ручных валидаторов7. Типовые расширения: кастомные сообщения и карта ошибок для фронтенда7.1. Локализация через type вместо парсинга текста7.2. Тестирование контракта ошибок8. Вывод: валидация — это UX, а не просто “422”