Из-за временных ограничений на территории РФ наблюдаются проблемы с оплатой. Если платёж не проходит, оставьте запрос в службу поддержки.Служба поддержки работает 24/7 — мы всегда на связи по вопросам хостинга и серверов.Открыт прием заявок на аренду выделенных серверов и размещение оборудования в дата-центре.Напоминаем: рекомендуем включить резервное копирование для дополнительной защиты данных.Доступна новая линейка VPS/VDS с NVMe-дисками и увеличенной производительностью.Технические работы на части серверов завершены. Все сервисы работают в штатном режиме.
Статья8 мин чтенияПросмотры0

JSON Patch test: как не заменить цену после чужого изменения

Практический guide по RFC 6902: ставим test перед replace, воспроизводим свежую и устаревшую цену, проверяем атомарный отказ и различия типов JSON на CPython 3.12.

Комментарии 0

Измерительный индикатор проверяет чистую бирку товара перед точной регулировкой
В этой статье

Если API принимает JSON Patch, поставьте операцию test перед replace. Клиент сообщает не только новую цену, но и значение, которое он видел. Если цена уже изменилась, test завершается ошибкой, а весь PATCH не применяется. Так устаревший процесс не затирает более свежее изменение.

Этот приём работает только при явно заявленной поддержке media type application/json-patch+json. Он не превращает любой PATCH в JSON Patch и не доказывает наличие такого формата в конкретном методе 1С-Битрикс: Управление сайтом или внешнего сервиса.

Входные условия

  • В контракте API указан application/json-patch+json.
  • Клиент знает ожидаемое текущее значение поля.
  • Сервер применяет документ JSON Patch целиком, а не по частям.
  • Для изменения всей версии ресурса отдельно рассмотрен HTTP-заголовок If-Match.
  • Тест выполняется на учебном объекте без сети, рабочего магазина и персональных данных.

Минимальный запрос: проверить 1000, затем записать 900

Пусть интеграция прочитала товар SKU-42 с ценой 1000 и остатком 5. Перед отправкой другая система могла изменить цену — это и нужно обнаружить.

{"sku":"SKU-42","price":1000,"stock":5}

Документ JSON Patch содержит две операции. Сначала test требует, чтобы /price всё ещё равнялся 1000. Только после успешной проверки выполняется replace.

[{"op":"test","path":"/price","value":1000},{"op":"replace","path":"/price","value":900}]

Что именно сравнивает test

Правила сравнения задаёт RFC 6902, раздел test. Типы JSON должны совпасть; исключение — числа, которые сравниваются по числовому значению. Порядок элементов массива важен, а порядок свойств объекта — нет.

Фактическое значение

Ожидаемое значение

Результат

Причина

1000

1000.0

успех

оба значения — числа и численно равны

1000

значение типа string «1000»

отказ

число и строка — разные типы

true

1

отказ

boolean и number — разные типы

объект a=1, b=2

объект b=2, a=1

успех

порядок свойств объекта не важен

массив north, south

массив south, north

отказ

порядок массива значим

Для Python здесь есть подвох: True == 1 возвращает истину, хотя в JSON это boolean и number. Поэтому учебная модель сначала сравнивает типы JSON и лишь затем значения.

Воспроизводимая учебная модель

Проверка выполнена 20 октября 2026 года на CPython 3.12.14 только со стандартной библиотекой. Модель поддерживает test и replace для одного ключа объекта — ровно тот объём, который нужен для проверки механизма. Это не универсальная библиотека JSON Patch.

from copy import deepcopy from typing import Any class PatchRejected(Exception): pass def json_type(value: Any) -> str: if value is None: return "null" if isinstance(value, bool): return "boolean" if isinstance(value, (int, float)): return "number" if isinstance(value, str): return "string" if isinstance(value, list): return "array" if isinstance(value, dict): return "object" raise TypeError("not a JSON value") def json_equal(left: Any, right: Any) -> bool: if json_type(left) != json_type(right): return False if isinstance(left, list): return len(left) == len(right) and all( json_equal(a, b) for a, b in zip(left, right) ) if isinstance(left, dict): return left.keys() == right.keys() and all( json_equal(left[key], right[key]) for key in left ) return left == right def read_key(document: dict, path: str) -> Any: if not path.startswith("/") or "/" in path[1:]: raise PatchRejected("teaching model supports one object key") key = path[1:].replace("~1", "/").replace("~0", "~") if key not in document: raise PatchRejected(f"path not found: {path}") return document[key] def apply_patch(document: dict, operations: list[dict]) -> dict: candidate = deepcopy(document) for operation in operations: op = operation["op"] path = operation["path"] if op == "test": if not json_equal(read_key(candidate, path), operation["value"]): raise PatchRejected(f"test failed at {path}") elif op == "replace": read_key(candidate, path) key = path[1:].replace("~1", "/").replace("~0", "~") candidate[key] = deepcopy(operation["value"]) else: raise PatchRejected(f"unsupported operation: {op}") return candidate

Запускаем три показательных сценария: свежая цена, устаревшая цена и поздний отказ после уже выполненного во временной копии replace.

document = {"sku": "SKU-42", "price": 1000, "stock": 5} patch = [ {"op": "test", "path": "/price", "value": 1000}, {"op": "replace", "path": "/price", "value": 900}, ] print(apply_patch(document, patch)) stale = {"sku": "SKU-42", "price": 1050, "stock": 5} try: apply_patch(stale, patch) except PatchRejected as error: print(f"stale_price: reject: {error}; unchanged={stale['price'] == 1050}") late_failure = [ {"op": "replace", "path": "/price", "value": 900}, {"op": "test", "path": "/stock", "value": 99}, ] try: apply_patch(document, late_failure) except PatchRejected as error: print(f"atomic_failure: reject: {error}; unchanged={document['price'] == 1000}")

Наблюдаемый результат

{'sku': 'SKU-42', 'price': 900, 'stock': 5} stale_price: reject: test failed at /price; unchanged=True atomic_failure: reject: test failed at /stock; unchanged=True

Процесс завершился с кодом 0. В полном прогоне из восьми сценариев дополнительно подтверждены: 1000 равно 1000.0; строка 1000 не равна числу 1000; true не равно 1; перестановка свойств объекта не меняет равенство; перестановка элементов массива меняет.

Три сценария JSON Patch: успешная замена свежей цены, отказ для устаревшей цены и атомарный откат при поздней ошибке
Учебная модель: test разрешает замену при свежей цене, отклоняет устаревшую и не оставляет частичное изменение после позднего отказа.

Почему поздняя ошибка не оставляет половину изменения

JSON Patch прекращает обработку при неуспешной операции. Вместе с требованием атомарности HTTP PATCH это означает: если весь patch-документ не может быть применён, сервер не должен сохранять предыдущие операции из него. Это следует из RFC 6902 и RFC 5789.

В модели изменения выполняются в глубокой копии. Только успешный проход возвращает новый документ. Если test для stock ожидает 99 при фактическом 5, ранее выполненный replace цены остаётся внутри отброшенной копии; исходная цена сохраняется равной 1000.

Какой HTTP-статус ждать

Нельзя считать, что неуспешный test всегда означает один и тот же HTTP-код. Контракт конкретного API должен определить ответ и тело ошибки. RFC 5789 приводит 409 Conflict для конфликта состояния без отдельного HTTP-условия и 412 Precondition Failed для отказа явно переданного HTTP-условия, например If-Match.

Механизм

Где находится условие

Ориентир по отказу

JSON Patch test

в теле patch-документа

контракт API; конфликт состояния может быть 409

If-Match

в HTTP-заголовке

412 при несоответствии условия

Неподдерживаемый media type

Content-Type запроса

415, если формат не поддерживается

test и If-Match решают разные задачи

Операция test проверяет конкретное значение внутри JSON-документа. If-Match привязывает изменение к версии всего представления ресурса по ETag. Подробный воспроизводимый пример есть в материале ETag и If-Match: как не затереть складской остаток при двух обновлениях API.

Критерий

JSON Patch test

If-Match

Область проверки

одно или несколько значений JSON

версия представления ресурса

Где передаётся

тело application/json-patch+json

HTTP-заголовок

Когда полезен

условие на конкретное поле

отказ после любой смены версии

Можно ли сочетать

да

да

Если контракт позволяет оба механизма, If-Match защищает версию ресурса на уровне HTTP, а test документирует условие бизнес-операции внутри patch-документа. Один механизм не стоит молча выдавать за другой.

Как встроить проверку в интеграцию магазина

  • 1. Зафиксируйте точный endpoint, версию API, права и поддерживаемый media type.
  • 2. Прочитайте ресурс и сохраните ожидаемое значение поля; при наличии ETag сохраните и его.
  • 3. Соберите patch-документ: test перед соответствующим replace.
  • 4. Отправьте Content-Type application/json-patch+json; If-Match добавляйте только по договорённому контракту.
  • 5. При отказе не повторяйте тот же replace вслепую: заново прочитайте ресурс и разберите конфликт.
  • 6. После успеха прочитайте ресурс или используйте документированный ответ, чтобы подтвердить итоговое значение.

Приёмка

  • Свежая цена 1000: test проходит, итоговая цена 900.
  • Фактическая цена 1050: test отклоняется, цена остаётся 1050.
  • Поздний test складского остатка не проходит: ранее указанный replace цены не сохраняется.
  • Число 1000 и строка 1000 различаются.
  • true не принимается за число 1.
  • Одинаковые объекты с иным порядком свойств проходят test.
  • Массив с другим порядком элементов не проходит test.
  • Неизвестная операция и отсутствующий путь завершают patch ошибкой.
  • Журнал не содержит токенов, cookies, персональных данных и полного ответа внешней системы.

Границы примера

Учебный код не выполняет HTTP-запрос, не проверяет аутентификацию, TLS, лимит размера тела и конкретную реализацию сервера. В нём нет add, remove, move и copy, вложенных указателей и массивных индексов. Для рабочего проекта берите библиотеку и сервер с подтверждённой поддержкой RFC 6902, а этот пример используйте как набор проверок для test, типов JSON и атомарного отказа.

Критерий готовности простой: устаревшее ожидаемое значение делает конфликт видимым, ни одна предыдущая операция того же patch-документа не остаётся применённой, а интеграция перечитывает ресурс вместо слепого повтора.

Обсуждение 0

Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.

Пока никто не написал. Начни обсуждение.