Skip to content

JSON Patch operations

RFC 6902 JSON Patch op types + a typed JsonPatchOpAdapter for round-tripping ops through DHIS2's PATCH /api/{resource}/{uid} endpoint. The discriminator on op (add, remove, replace, move, copy, test) routes to the matching BaseModel subclass.

from dhis2w_client import JsonPatchOpAdapter

op = JsonPatchOpAdapter.validate_python({"op": "replace", "path": "/name", "value": "ANC 2024"})
# -> ReplaceOp(op='replace', path='/name', value='ANC 2024')

await client.resources.programs.patch(program_uid, [op])

The adapter accepts both Python dicts and JSON strings; outputs dump back via .model_dump() / .model_dump_json(). Property-based tests in tests/test_parser_properties.py cover the round-trip and discriminator dispatch for every variant.

When to reach for this

  • Bulk-patching one field across many resources where a full PUT round-trip is wasteful.
  • Server-side conditional ops (test followed by replace in one batch).
  • Custom patch generators (CSV-driven sweeps, drift-detection auto-fixes).

For the higher-level batch patcher see client.metadata.patch_bulk on Metadata accessor.

json_patch

Typed RFC 6902 JSON Patch operations — reusable across every DHIS2 PATCH endpoint.

DHIS2 accepts JSON Patch bodies on PATCH /api/<resource>/{id} across most metadata types (routes, users, data elements, ...). Each op is a distinct pydantic class tagged by the op field; the discriminated Union makes pydantic route incoming dicts to the right variant and reject wrong-shape bodies at construction time.

Variant shapes (per RFC 6902):

add      {op, path, value}           insert / overlay at path
remove   {op, path}                  delete at path
replace  {op, path, value}           overwrite at path with value
test     {op, path, value}           assert equality; aborts on mismatch
move     {op, path, from}            move value from -> path
copy     {op, path, from}            copy value from -> path

from is a Python reserved word, so it's aliased to the from_ attribute. JsonPatchOpAdapter.validate_python(raw) picks the right variant from a dict; op.model_dump(by_alias=True) serialises back to the wire shape.

Attributes

JsonPatchOp = Annotated[AddOp | RemoveOp | ReplaceOp | TestOp | MoveOp | CopyOp, Field(discriminator='op')] module-attribute

Discriminated union over the six RFC 6902 ops. Validates {op, path, value?, from?} per op shape.

JsonPatchOpAdapter = TypeAdapter(JsonPatchOp) module-attribute

Classes

AddOp

Bases: _JsonPatchBase

RFC 6902 add — insert value at path (or replace existing on overlap).

Source code in packages/dhis2w-client/src/dhis2w_client/v42/json_patch.py
class AddOp(_JsonPatchBase):
    """RFC 6902 `add` — insert `value` at `path` (or replace existing on overlap)."""

    op: Literal["add"] = "add"
    value: Any

RemoveOp

Bases: _JsonPatchBase

RFC 6902 remove — delete the value at path.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/json_patch.py
class RemoveOp(_JsonPatchBase):
    """RFC 6902 `remove` — delete the value at `path`."""

    op: Literal["remove"] = "remove"

ReplaceOp

Bases: _JsonPatchBase

RFC 6902 replace — overwrite the value at path with value.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/json_patch.py
class ReplaceOp(_JsonPatchBase):
    """RFC 6902 `replace` — overwrite the value at `path` with `value`."""

    op: Literal["replace"] = "replace"
    value: Any

TestOp

Bases: _JsonPatchBase

RFC 6902 test — assert the value at path equals value (aborts the patch on mismatch).

Source code in packages/dhis2w-client/src/dhis2w_client/v42/json_patch.py
class TestOp(_JsonPatchBase):
    """RFC 6902 `test` — assert the value at `path` equals `value` (aborts the patch on mismatch)."""

    # Tell pytest this isn't a test class (naming collision with the Test* convention).
    __test__ = False

    op: Literal["test"] = "test"
    value: Any

MoveOp

Bases: _JsonPatchBase

RFC 6902 move — move the value at from_ to path. Serialises from_ as from on the wire.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/json_patch.py
class MoveOp(_JsonPatchBase):
    """RFC 6902 `move` — move the value at `from_` to `path`. Serialises `from_` as `from` on the wire."""

    op: Literal["move"] = "move"
    from_: str = Field(alias="from")

CopyOp

Bases: _JsonPatchBase

RFC 6902 copy — copy the value at from_ to path. Serialises from_ as from on the wire.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/json_patch.py
class CopyOp(_JsonPatchBase):
    """RFC 6902 `copy` — copy the value at `from_` to `path`. Serialises `from_` as `from` on the wire."""

    op: Literal["copy"] = "copy"
    from_: str = Field(alias="from")