Skip to content

Registries and Routes

A registry is the set of operations a route will accept. If an operation is not in the registry, the request fails during validation before any mutation runs.

Registries are a practical way to apply least privilege to mutation surfaces.

Good patterns include:

  • a narrow public registry
  • a broader internal registry
  • a dev-only registry for experiments
  • a superuser route with elevated auth and a wider registry

Why Route-Specific Registries Matter

This is about route-level control. You don’t need custom operations for this to matter.

  • Security boundaries - Some operations are safe internally but risky to expose. For example, test can be abused to probe for values a client should not be able to observe.
  • Different audiences - Public, partner, internal, and admin routes often need different mutation vocabularies, even over the same resource.
  • Safer evolution - New or experimental operations can be introduced in internal routes first without committing them to public APIs.

Create Registries

It's as simple as this:

from jsonpatchx import AddOp, RemoveOp, ReplaceOp, MoveOp, CopyOp, TestOp

type WriteOnlyRegistry = AddOp | RemoveOp | ReplaceOp | MoveOp | CopyOp
type StandardRegistry = WriteOnlyRegistry | TestOp

Both JsonPatch and JsonPatchFor use StandardRegistry (the six standard RFC 6902 operations) by default.

But you can use your own:

JsonPatch(ops, registry=WriteOnlyRegistry).apply(doc)
@app.patch("/users/{user_id}", response_model=User)
def patch_user(user_id: int, patch: JsonPatchFor[User, WriteOnlyRegistry]) -> User:
    ...

Environment-Specific Registries

You can load operations from a startup-time configuration source and build a registry using Union[*ops]:

from typing import Union

registry_ops = [operation_by_name[name] for name in load_config("registry.json")]
type MyRegistry = Union[*registry_ops]

Type checkers will complain about runtime-generated types like this because they can't reason about them. You can safely use # type: ignore here.

You can also vary registries based on environment or deployment context:

import os
from typing import Union
from jsonpatchx import StandardRegistry

extras = [operation_by_name[name] for name in names if os.getenv(name)]
type MyRegistry = Union[StandardRegistry, *extras]

This makes registry changes deployment-driven: you can enable, disable, or roll back operations by changing config or environment flags and restarting or redeploying the application.