В контракте частичного обновления профиля отсутствие поля должно означать «не менять», а явно переданное пустое значение — «очистить». Как спроектировать семантику запроса, чтобы сервер не путал эти случаи?
Нужно явно зафиксировать трёхзначную семантику каждого изменяемого поля: поле отсутствует — оставить текущее значение, поле передано со значением — заменить его, поле передано с пустым значением — очистить его. Сервер должен различать отсутствие свойства и наличие свойства с пустым значением, а контракт должен описывать это поведение отдельно для каждого поля.
Проблема возникла из различия между полной заменой ресурса и частичным обновлением. При полной замене клиент передаёт новое представление объекта целиком, а при частичном обновлении передаётся только набор изменений.
Если использовать одну и ту же модель данных для обоих случаев, после десериализации легко потерять информацию о том, было ли поле пропущено или явно передано пустым. Поэтому API частичного обновления должен сохранять признак присутствия каждого поля до применения изменений.
Пусть у профиля есть имя, телефон и фотография. Клиент хочет изменить только имя. Если сервер трактует пропущенные поля как пустые, он случайно удалит телефон и фотографию. Если сервер трактует любое пустое значение как «поле не менялось», клиент не сможет очистить телефон или удалить фотографию.
Ошибка особенно опасна при обновлении старым клиентом: он может не знать о новых полях и не передавать их. Некорректная семантика приведёт либо к потере данных, либо к невозможности выполнить легитимное удаление значения.
В контракте нужно определить правила обработки каждого состояния поля:
Пример частичного обновления:
В этом примере имя заменяется, телефон очищается, а отсутствие поля avatar означает, что аватар остаётся без изменений. Такой контракт требует, чтобы сервер различал отсутствие свойства и его наличие со значением null; обычное приведение запроса к объекту с заранее заполненными значениями может эту информацию уничтожить.
Для каждого поля следует отдельно описать допустимость пустого значения, правила валидации и результат очистки. Если пустое значение само является легитимным содержимым, его нельзя одновременно использовать как команду удаления: потребуется отдельная операция, специальный маркер или формат патча с явными действиями.
PUT обычно моделирует замену представления ресурса, поэтому пропущенное поле в нём не следует автоматически трактовать как «не менять», если это не закреплено специальным контрактом. Для частичных изменений лучше использовать явно определённую семантику PATCH или другого метода обновления.
Валидацию нужно выполнять после применения изменений к текущему состоянию, если ограничения относятся к итоговому объекту. Например, проверка обязательности пары связанных полей должна учитывать и старые значения, которые не были переданы, и новые значения из запроса.
Важно также описать поведение при неизвестных полях. Обычно их отклоняют, чтобы опечатка не выглядела как успешно принятое изменение, но иногда допустим режим игнорирования для совместимости. Выбор должен быть единообразным и отражён в публичном контракте.
В профиле пользователя телефон можно изменить или удалить. Мобильное приложение отправляло только изменённые поля, а серверная модель после разбора запроса содержала пустые значения для всех отсутствующих свойств. В результате изменение имени периодически очищало телефон.
Рассматривались три варианта. Полная отправка профиля упрощала серверную логику, но увеличивала размер запросов и требовала от клиента знать все поля, включая добавленные позднее. Использование пустого значения как универсальной команды было простым, но не позволяло отличить очистку от отсутствия изменения. Явная семантика частичного обновления сохраняла гибкость, однако требовала поддержки присутствия полей и более подробной документации.
Выбрали третий вариант: отсутствие свойства означало «не менять», null — «очистить» только для полей, где это разрешено, а для остальных полей null отклонялся валидацией. Это устранило неоднозначность и позволило старым клиентам безопасно обновлять только известные им поля.
Нет, без явного решения это опасно. Для операции полной замены пропущенное поле обычно означает, что новое представление его не содержит, поэтому сервер может удалить старое значение или применить значение по умолчанию. Если команда хочет трактовать пропуск как «не менять» и при полной замене, это уже нестандартная договорённость, которую нужно явно документировать; по смыслу операция становится частичным обновлением.
Нельзя перегружать один маркер двумя смыслами. Например, если пустая строка может быть настоящим значением, для удаления следует использовать отдельную операцию или явное поле действия. Иначе сервер не сможет определить, хочет клиент сохранить пустую строку или удалить значение совсем.
Нужно сформировать предполагаемое итоговое состояние: взять текущий ресурс, применить к нему только присутствующие в запросе изменения, затем проверить ограничения и сохранить результат. Проверка только тела патча может ошибочно отклонить запрос из-за отсутствующих полей или, наоборот, пропустить некорректную комбинацию старых и новых значений. При конкурентных обновлениях дополнительно нужна проверка версии или другая стратегия согласования, иначе валидное изменение может быть применено поверх устаревшего состояния.