Прямой ответ
Метод PATCH обновляет ресурс частично, но HTTP не определяет формат тела — его задаёт стандарт. Два основных: JSON Merge Patch (RFC 7386) и JSON Patch (RFC 6902). Они решают разные задачи и не взаимозаменяемы.
| Свойство | JSON Merge Patch | JSON Patch |
|---|---|---|
| Формат тела | Объект с изменяемыми полями | Массив операций |
| Удаление поля | Значение null |
Операция remove |
Установка null как значения |
Невозможно | Операция replace |
| Изменение элемента массива | Замена всего массива | Операция по индексу |
| Идемпотентность | Да | Не гарантирована |
| Проверка предусловия | Операция test |
Операция test |
JSON Merge Patch — тело содержит только изменяемые поля:
PATCH /api/v1/orders/1001
Content-Type: application/merge-patch+json
{"status": "shipped", "comment": null}
Поле status обновляется, поле comment удаляется. Ключевое ограничение: null в этом формате означает «удалить», поэтому установить значение null нельзя — различие между «поле отсутствует» и «поле равно null» теряется.
JSON Patch — массив операций с путями:
PATCH /api/v1/orders/1001
Content-Type: application/json-patch+json
[
{"op": "replace", "path": "/status", "value": "shipped"},
{"op": "replace", "path": "/comment", "value": null},
{"op": "test", "path": "/version", "value": 7}
]
Формат различает удаление (remove) и установку null (replace), позволяет работать с отдельными элементами массива и поддерживает операцию test, которая прерывает применение при невыполнении условия.
Операция test реализует оптимистичную блокировку на уровне запроса:
[{"op": "test", "path": "/version", "value": 7},
{"op": "replace", "path": "/status", "value": "shipped"}]
Если версия изменилась, весь пакет не применяется и возвращается ошибка; о подходе в базе данных — в статье про оптимистичную блокировку.
Альтернативный механизм — заголовок If-Match с ETag, определённый в HTTP: сервер применяет изменение только при совпадении версии ресурса.
Обработка ошибок и валидация
Частичное обновление требует более аккуратной обработки ошибок, чем полная замена ресурса, поскольку некорректное состояние может возникнуть из сочетания отдельных полей.
Проверяйте допустимые поля. Список полей, которые клиент вправе изменить, задаётся на сервере. Попытка изменить поле вне списка должна отклоняться с кодом 403, а не игнорироваться молча: молчаливое игнорирование скрывает ошибку клиента и приводит к расхождению ожиданий.
Валидируйте итоговое состояние, а не только вход. Обновление одного поля может нарушить бизнес-правило, зависящее от нескольких полей. Например, установка даты отгрузки без указания способа доставки делает заказ некорректным, хотя каждое поле по отдельности допустимо.
Различайте ошибки формата и ошибки состояния. Некорректный путь в JSON Patch — ошибка формата (400), а недопустимый переход статуса — ошибка состояния (409 или 422). Смешение этих случаев усложняет обработку на клиенте.
Описывайте ошибки структурированно. Формат описания ошибок позволяет указать, какое именно поле или операция вызвали отказ:
{"type": "about:blank", "title": "Validation failed", "status": 422,
"errors": [{"pointer": "/status", "message": "invalid_transition"}]}
Для JSON Patch полезно указывать индекс операции в массиве, чтобы клиент мог повторить пакет без неверного элемента.
Применяйте операции атомарно. Если одна операция в массиве не выполнена, весь массив не применяется. Частичное применение массива операций оставляет ресурс в состоянии, которое клиент не запрашивал и не ожидает.
Ограничьте число операций в одном запросе. Массив из тысяч операций выполняется дольше таймаута клиента и удерживает блокировку ресурса. Предельное число операций указывается в спецификации наравне с предельным размером тела запроса.
Практика: выбор формата и его реализация
1. Выбирайте Merge Patch для простых случаев. Обновление нескольких полей верхнего уровня не требует операций, а формат понятен клиентам.
2. Используйте JSON Patch при работе с массивами. Замена всего массива из-за изменения одного элемента приводит к конфликтам при одновременном редактировании.
3. Указывайте тип содержимого. application/merge-patch+json и application/json-patch+json — разные форматы, и сервер обязан их различать.
4. Обрабатывайте неизвестные поля явно. Игнорирование неизвестного поля скрывает ошибку клиента; о совместимости — в статье про версии API.
5. Проверяйте права на каждое поле. Частичное обновление не должно позволять изменить поля, недоступные роли пользователя.
6. Фиксируйте семантику в контракте. Какой формат поддерживается и как трактуется null — часть спецификации; о контрактах — в статье про OpenAPI.
7. Возвращайте обновлённый ресурс. Клиент получает актуальное состояние и вычисленные сервером поля без дополнительного запроса.
Рекомендации:
- Не смешивайте два формата в одном API. Поддержка обоих без явного различения типа содержимого приводит к неверной трактовке
null. - Тестируйте удаление поля. Случай «поле отсутствует» и «поле равно null» различаются, и тесты должны это покрывать.
- Ограничьте глубину вложенности. Глубоко вложенный merge patch сложен для понимания и отладки.
- Валидируйте результат, а не только вход. Частичное обновление может привести к состоянию, недопустимому по бизнес-правилам.
- Логируйте применённые операции. Для JSON Patch журнал операций полезен при разборе инцидентов.
Практический сценарий: потерянные значения null из-за неверной трактовки формата
Контекст: клиент отправлял {"discount": null}, чтобы установить скидку в ноль. Сервер интерпретировал это как удаление поля, и скидка пересчитывалась по умолчанию.
- Диагностика. В логах запрос содержал корректное тело, а в базе поле
discountполучало значениеNULLвместо ожидаемого0. Различия между отсутствием поля и значениемnullв реализации не было. - Причина. Сервер принимал JSON Merge Patch, где
nullозначает удаление, а клиент ожидал семантику установки значения. Тип содержимого не проверялся, поэтому оба формата обрабатывались одинаково. - Действия. (а) Для ресурсов, где значение
nullзначимо, добавили поддержку JSON Patch с операциейreplace; (б) тип содержимого стали различать: merge patch и json patch обрабатываются разными функциями; (в) для установки нуля ввели явное значение0вместоnull, поскольку семантика стала однозначной; (г) добавили тесты на три случая: поле отсутствует, поле удаляется, поле устанавливается вnull; (д) поведение описали в спецификации с примерами для каждого формата. - Результат. Неоднозначность устранена, а тесты фиксируют различие между отсутствием поля и значением
null.
Вывод: выбор формата PATCH определяет семантику null и возможность работы с массивами. Отсутствие явного различения типов содержимого приводит к потере данных, которую сложно обнаружить, поскольку запрос формально выполняется успешно.
FAQ
Какой формат выбрать по умолчанию? JSON Merge Patch для обновления полей верхнего уровня. JSON Patch — когда нужны удаление, работа с массивами или проверка предусловия.
Можно ли установить значение null через Merge Patch?
Нет. В этом формате null означает удаление поля. Для установки значения нужен JSON Patch с операцией replace.
Идемпотентен ли PATCH? JSON Merge Patch идемпотентен: повторное применение даёт тот же результат. JSON Patch не идемпотентен для операций вроде добавления элемента в массив.
Как защититься от одновременного обновления?
Операцией test по версии либо заголовком If-Match с ETag. Оба механизма отклоняют изменение при изменённой версии ресурса.
Источники: RFC 7386 (JSON Merge Patch), RFC 6902 (JSON Patch), RFC 9110 (HTTP Semantics) — ссылки в шапке.