FastAPI и Pydantic: как проверить, что конец интервала позже начала
Бронирование содержит две корректные даты, но заканчивается раньше, чем начинается. Каждая дата отдельно прошла проверку типа. Ошибка появилась в отношении между полями: сервер разрешил интервал, который невозможно использовать в расписании. Это правило нужно проверять для всей модели до обращения к базе.
Разделите форму интервала и занятость комнаты
Сначала проверяется, что обе границы являются датами с часовым поясом и end строго больше start. Затем сервис проверяет доступность комнаты в базе. Первая ошибка зависит только от присланного документа и даёт 422. Занятый интервал зависит от текущих записей и обычно означает конфликт 409. Смешивание этих проверок в одном валидаторе усложняет тесты и привязывает схему к базе.
Pydantic model_validator(mode="after") получает уже разобранный объект. Внутри можно сравнивать даты, а не исходные строки. Верните self, если правило выполнено, и выбросьте ValueError при нарушении. Не используйте assert для пользовательской валидации: запуск Python с оптимизацией может удалить такие проверки. В тестах assert, наоборот, уместен.
Задайте часовой пояс частью договора
Строки 2026-11-02T10:00:00+03:00 и 2026-11-02T07:00:00Z обозначают один момент. Тип AwareDatetime требует смещение и позволяет сравнивать реальные моменты времени. Без этой договорённости серверу пришлось бы угадывать, какое местное время прислал клиент. Пример возвращает длительность в секундах, вычисленную из проверенных дат.
Пример рассчитан на Python 3.11+ с FastAPI, Pydantic v2 и HTTPX. Установите пакеты fastapi и httpx в отдельное окружение, сохраните блок в файл и запустите его Python.
from typing import Self
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, AwareDatetime, model_validator
app = FastAPI()
class Interval(BaseModel):
start: AwareDatetime
end: AwareDatetime
@model_validator(mode="after")
def ordered(self) -> Self:
if self.end <= self.start:
raise ValueError("end must be later than start")
return self
@app.post("/intervals")
def check_interval(data: Interval):
return {"seconds": (data.end - data.start).total_seconds()}
with TestClient(app) as client:
good = {"start": "2026-11-02T10:00:00+03:00", "end": "2026-11-02T08:00:00Z"}
response = client.post("/intervals", json=good)
assert response.status_code == 200
assert response.json() == {"seconds": 3600.0}
for end in ["2026-11-02T07:00:00Z", "2026-11-02T06:59:00Z"]:
assert client.post("/intervals", json={**good, "end": end}).status_code == 422
assert client.post("/intervals", json={**good, "start": "2026-11-02T10:00:00"}).status_code == 422
print("timezones, equal borders and reverse interval checked")Почему важны граничные примеры
Обычный часовой интервал проверяет только счастливый путь. Равные границы обнаруживают случайное использование знака меньше вместо меньше или равно. Обратный порядок проверяет сам запрет, а разные смещения часового пояса показывают, что сравниваются даты, а не строки. Запрос без смещения проверяет договор с клиентом до выполнения обработчика.
Ошибка model_validator относится ко всей модели, поэтому её loc обычно содержит body без имени одного конкретного поля. Интерфейсу удобно показывать такую ошибку рядом с группой даты и времени. Если нужен стабильный машинный код предметной ошибки, добавьте его осмысленно через собственный тип ошибки; не заставляйте клиент разбирать английскую фразу сообщения.
Как перейти к реальному расписанию
После валидации интервала база проверяет пересечение с существующими бронированиями. Для полуоткрытого интервала [start, end) соседние брони могут соприкасаться границами. Условие пересечения требует, чтобы начало одной брони было раньше конца другой и наоборот. Эта договорённость должна совпадать в API, запросах и тестах. Здесь мы проверяем только корректность одного присланного интервала.
Не обращайтесь к сети из model_validator, чтобы узнать календарь пользователя или рабочие часы внешнего сервиса. Проверенная схема должна оставаться предсказуемым входом в бизнес-логику. Правила, которым нужны актуальные данные, вынесите в отдельную функцию сервиса или зависимость. Тогда ошибку даты можно воспроизвести одним HTTP-запросом, а конфликт брони проверить с нужным состоянием временной базы.
Следующая практика
Начните с открытого урока о POST и Pydantic: создайте модель запроса и убедитесь, что невалидный документ не доходит до обработчика. После этой базы удобно добавлять собственные правила между полями, как в проверке интервала. Откройте бесплатный урок и выполните практику.
В бесплатном курсе FastAPI вы затем соедините маршруты, базу, регистрацию и тесты в одном приложении. Когда понадобится воспроизводимо запускать этот backend на другом компьютере, откройте программу Docker для разработчика: в ней есть образы, контейнеры и Compose для приложения с базой.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
