Итог заказа устарел: вычисляем поле ответа через computed_field
API хранит в модели заказа позиции и total. При изменении количества разработчик обновляет список, но забывает пересчитать итог. JSON проходит сериализацию, типы правильные, а клиент видит сумму от предыдущего состояния. Если итог полностью определяется текущими позициями, отдельное изменяемое поле создаёт лишнее состояние.
Пусть ответ вычисляет производное значение
computed_field включает свойство модели в сериализацию Pydantic. Источником остаются позиции, а total_cents вычисляется из количества и цены. Используем целое число минимальных денежных единиц: в этом примере не нужны дробные ставки и правила округления. Для налога или скидок с дробями сначала нужно отдельно определить денежную арифметику.
Модель ответа не обязана совпадать с командой создания заказа. Здесь клиент не передаёт итог: сервер строит Receipt из позиций, а свойство вычисляет результат. Проверяем его как часть настоящего ответа FastAPI, чтобы заметить забытый декоратор computed_field. Обычный property доступен Python-коду, но сам по себе не гарантирует поле в JSON.
Один источник суммы и проверка изменения
Понадобятся fastapi, httpx и Pydantic v2. Сначала создадим локальную модель и изменим количество, затем проверим HTTP-маршрут с отдельным экземпляром. Это два полезных сценария: вычисление вслед за данными и подключение сериализации на границе API.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, computed_field
class Line(BaseModel):
quantity: int = Field(gt=0)
price_cents: int = Field(ge=0)
class Receipt(BaseModel):
lines: list[Line]
@computed_field
@property
def total_cents(self) -> int:
return sum(line.quantity * line.price_cents for line in self.lines)
receipt = Receipt(lines=[Line(quantity=2, price_cents=150)])
assert receipt.model_dump()["total_cents"] == 300
receipt.lines[0].quantity = 3
assert receipt.model_dump()["total_cents"] == 450
try:
Line(quantity=0, price_cents=150)
except ValueError:
pass
else:
raise AssertionError("zero quantity accepted")
app = FastAPI()
@app.get("/receipt", response_model=Receipt)
def get_receipt():
return Receipt(lines=[Line(quantity=2, price_cents=150),
Line(quantity=1, price_cents=50)])
with TestClient(app) as client:
response = client.get("/receipt")
assert response.status_code == 200
assert response.json()["total_cents"] == 350
print(response.json())
Где заканчивается вычисляемое поле
Свойство подходит для дешёвого детерминированного расчёта из уже имеющихся данных. SQL-запрос внутри свойства превратит простую сериализацию в скрытую сетевую работу. Для итогов по тысячам строк лучше заранее получить агрегат из базы и явно построить модель ответа. Тогда место и цена вычисления останутся видимыми в сервисном слое.
Не добавляйте cached_property, если модель и вложенные позиции продолжают меняться: кэш сохранит прежнюю сумму. В приведённом варианте property пересчитывается при обращении. Если объект неизменяемый и расчёт дорогой, кэширование можно рассмотреть отдельно, обеспечив неизменяемость всего вложенного состояния. Одного frozen у внешней модели для этого недостаточно.
Для принятого платежа итог нередко является историческим фактом. Его нельзя автоматически заменить вычислением по сегодняшней цене товара. Сохраняйте цены и правила на момент операции, а вычисляемое поле используйте для представления согласованных данных. Решение о хранении итога определяется предметной областью, а не удобством декоратора.
В OpenAPI вычисляемое поле относится к выходному представлению и отмечается как доступное для чтения. Отдельная модель команды помогает не путать принимаемые значения с производными. При расширении ответа проверьте точные имена полей: отсутствие total в HTTP-тесте должно падать, даже если локальный Python-тест свойства успешно проходит.
Проверяйте входные ограничения отдельно
В примере изменение количества происходит на заведомо допустимое число. По умолчанию присваивание атрибуту модели после создания не повторяет всю проверку входа. Если приложение меняет такие объекты произвольно, выберите проверку присваивания или создание новой модели из команды изменения. Иначе вычисляемый итог будет точно отражать уже испорченное состояние, например отрицательное количество.
Для чека добавьте тест пустого набора позиций и нескольких одинаковых цен. Решите, допускается ли пустой чек вообще: суммирование возвращает ноль, но продукт может требовать хотя бы одну позицию. Это ограничение относится к полю lines и выражается отдельно. Так математически корректный результат не станет случайным разрешением операции, которую бизнес считает неполной.
Продолжить на практике
Следующая практика — отделить данные команды от представления ответа и протестировать маршрут. Бесплатный курс FastAPI соединяет Pydantic-модели с CRUD и TestClient; на этой базе удобно закрепить один источник данных для ответа. Откройте бесплатную программу FastAPI.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
