Payments are experiencing issues due to temporary restrictions in Russia. If your payment does not go through, please submit a support request.Our support team is available 24/7 — we are always here to help with hosting and server issues.We are now accepting requests for dedicated server rental and colocation services in our data center.Reminder: we recommend enabling backups for additional data protection.A new VPS/VDS lineup with NVMe storage and improved performance is now available.Maintenance work on some servers has been completed. All services are operating normally.
Article8 min readViews0

JSON Patch test: how not to overwrite a price after someone else's change

Practical guide to RFC 6902: place a test before replace, reproduce fresh and stale prices, verify atomic failure and JSON type differences on CPython 3.12.

Comments 0

The measurement indicator checks the product's clean label before precise adjustment
In this article

If the API accepts JSON Patch, place a test operation before replace. The client reports not only the new price but also the value it saw. If the price has already changed, the test fails with an error, and the entire PATCH is not applied. This prevents a stale process from overwriting a more recent change.

This technique works only when the media type application/json-patch+json is explicitly declared. It does not convert any PATCH into a JSON Patch, nor does it prove the existence of such a format in a specific 1C-Bitrix: Site Management method or an external service.

Input conditions

  • The API contract specifies application/json-patch+json.
  • The client knows the expected current value of the field.
  • The server applies the JSON Patch document in its entirety, not in parts.
  • For changing the entire resource version, the HTTP If-Match header is considered separately.
  • The test runs on a training object without network access, a live store, or personal data.

Minimal request: check 1000, then write 900

Suppose the integration read product SKU-42 with a price of 1000 and a stock of 5. Before sending, another system might have changed the price—that is exactly what needs to be detected.

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

The JSON Patch document contains two operations. First, the test operation requires that /price still equals 1000. Only after a successful check is the replace operation executed.

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

What exactly does test compare?

Comparison rules are defined by RFC 6902, section test. JSON types must match; the exception is numbers, which are compared by numeric value. The order of array elements matters, but the order of object properties does not.

Actual value

Expected value

Result

Reason

1000

1000.0

success

both values are numbers and numerically equal

1000

string value "1000"

failure

number and string are different types

true

1

failure

boolean and number are different types

object a=1, b=2

object b=2, a=1

success

the order of object properties does not matter

array north, south

south, north array

failure

array order matters

There is a trap in Python here: True == 1 returns true, even though in JSON these are distinct boolean and number types. Therefore, the educational model first compares JSON types and only then compares values.

Reproducible educational model

The check was performed on October 20, 2026, on CPython 3.12.14 using only the standard library. The model supports test and replace for a single object key—exactly the scope needed to verify the mechanism. This is not a universal JSON Patch library.

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

We run three illustrative scenarios: a fresh price, an outdated price, and a late failure after a replace has already been executed in a temporary copy.

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}")

Observed result

{'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

The process completed with code 0. In a full run of eight scenarios, the following were additionally confirmed: 1000 equals 1000.0; string 1000 does not equal number 1000; true does not equal 1; reordering object properties does not change equality; reordering array elements changes equality.

Three JSON Patch scenarios: successful replacement of a fresh price, rejection for an outdated price, and atomic rollback on a late error.
Educational model: test allows replacement with a fresh price, rejects an outdated price, and leaves no partial change after a late failure.

Why a late error leaves no half-change

JSON Patch stops processing upon a failed operation. Combined with the atomicity requirement of HTTP PATCH, this means: if the entire patch document cannot be applied, the server must not persist any preceding operations from it. This follows from RFC 6902 and RFC 5789.

In the model, changes are executed on a deep copy. Only a successful pass returns the new document. If test for stock expects 99 but the actual value is 5, the previously executed price replace remains inside the discarded copy; the original price remains equal to 1000.

Which HTTP status to expect

Do not assume that an unsuccessful test always results in the same HTTP code. The specific API contract must define the response and error body. RFC 5789 specifies 409 Conflict for a state conflict without a separate HTTP condition and 412 Precondition Failed for rejection of an explicitly passed HTTP condition, such as If-Match.

Mechanism

Where the condition is located

Failure reference

JSON Patch test

in the body of the patch document

API contract; a state conflict may result in 409

If-Match

in the HTTP header

412 if the condition does not match

Unsupported media type

Content-Type of the request

415 if the format is not supported

test and If-Match solve different tasks

The test operation checks a specific value within a JSON document. If-Match binds the change to the version of the entire resource representation via ETag. A detailed, reproducible example is available in the article ETag and If-Match: how not to overwrite stock levels during two API updates.

Criterion

JSON Patch test

If-Match

Check scope

one or more JSON values

resource representation version

Where it is passed

body of application/json-patch+json

HTTP header

When useful

condition on a specific field

failure after any version change

Can they be combined

yes

yes

If the contract allows both mechanisms, If-Match protects the resource version at the HTTP level, while test documents the business operation condition inside the patch document. One mechanism must not be silently passed off as the other.

How to integrate a check into a store integration

  • 1. Record the exact endpoint, API version, permissions, and supported media type.
  • 2. Read the resource and save the expected field value; if an ETag is present, save that as well.
  • 3. Assemble the patch document: place test before the corresponding replace.
  • 4. Send Content-Type application/json-patch+json; add If-Match only per the agreed contract.
  • 5. On failure, do not blindly repeat the same replace: re-read the resource and resolve the conflict.
  • 6. On success, read the resource or use the documented response to confirm the final value.

Acceptance testing

  • Fresh price 1000: test passes, final price 900.
  • Actual price 1050: test fails, price remains 1050.
  • Late warehouse stock test fails: the previously specified price replace is not saved.
  • The number 1000 and the string 1000 are different.
  • true is not accepted as the number 1.
  • Identical objects with different property order pass test.
  • An array with a different element order fails test.
  • An unknown operation or a missing path ends the patch with an error.
  • The log contains no tokens, cookies, personal data, or the full response from the external system.

Example boundaries

The test code does not perform HTTP requests, does not check authentication, TLS, body size limits, or specific server implementations. It lacks add, remove, move, and copy operations, nested pointers, and array indices. For a production project, use a library and server with verified RFC 6902 support, and use this example as a set of checks for test, JSON types, and atomic failure.

The readiness criterion is simple: an outdated expected value makes the conflict visible, no previous operation of the same patch document remains applied, and the integration re-reads the resource instead of blindly repeating.

Discussion 0

Share your experience and ask questions. Comments without links appear after editorial review.

No comments yet. Start the discussion.