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