Skip to content

JSON Patch

RFC 6902 PATCH support. Requires the jsonpatch extra:

pip install 'fastapi-views[jsonpatch]'

The view mixin lives in fastapi_views.views.jsonpatch; the request/patch models live in fastapi_views.models.jsonpatch. A patch document arrives as application/json-patch+json, is applied to the current representation, and the result is persisted through the repository.

For a complete walkthrough see JSON Patch.


Views

JsonPatchViewMixin

Source code in fastapi_views/views/jsonpatch.py
class JsonPatchViewMixin:
    partial_update_schema: type[BaseModel]

    def apply_patch(self, obj: Any, operations: JsonPatchModel) -> dict[str, Any]:
        """Patch ``obj`` and return only the changed, schema-validated fields.

        The object is projected onto ``partial_update_schema`` and dumped in
        JSON mode, as patch operations compare against raw JSON values. The
        patched document is validated again so the update cannot produce an
        invalid resource; a patch or validation failure, or a patch touching
        fields outside the schema, maps to ``400``.
        """
        schema = self.partial_update_schema
        doc = schema.model_validate(obj, from_attributes=True).model_dump(mode="json")
        try:
            patched_doc = operations.apply(doc)
            patched = schema.model_validate(patched_doc)
        except (
            jsonpatch.JsonPatchException,
            jsonpointer.JsonPointerException,
            ValidationError,
        ):
            raise BadRequest("Invalid operations") from None
        changed = {
            field
            for field in doc.keys() | patched_doc.keys()
            if doc.get(field, _MISSING) != patched_doc.get(field, _MISSING)
        }
        unknown = changed - schema.model_fields.keys()
        if unknown:
            msg = f"Unknown fields: {', '.join(sorted(unknown))}"
            raise BadRequest(msg)
        return patched.model_dump(include=changed)

apply_patch(obj: Any, operations: JsonPatchModel) -> dict[str, Any]

Patch obj and return only the changed, schema-validated fields.

The object is projected onto partial_update_schema and dumped in JSON mode, as patch operations compare against raw JSON values. The patched document is validated again so the update cannot produce an invalid resource; a patch or validation failure, or a patch touching fields outside the schema, maps to 400.

Source code in fastapi_views/views/jsonpatch.py
def apply_patch(self, obj: Any, operations: JsonPatchModel) -> dict[str, Any]:
    """Patch ``obj`` and return only the changed, schema-validated fields.

    The object is projected onto ``partial_update_schema`` and dumped in
    JSON mode, as patch operations compare against raw JSON values. The
    patched document is validated again so the update cannot produce an
    invalid resource; a patch or validation failure, or a patch touching
    fields outside the schema, maps to ``400``.
    """
    schema = self.partial_update_schema
    doc = schema.model_validate(obj, from_attributes=True).model_dump(mode="json")
    try:
        patched_doc = operations.apply(doc)
        patched = schema.model_validate(patched_doc)
    except (
        jsonpatch.JsonPatchException,
        jsonpointer.JsonPointerException,
        ValidationError,
    ):
        raise BadRequest("Invalid operations") from None
    changed = {
        field
        for field in doc.keys() | patched_doc.keys()
        if doc.get(field, _MISSING) != patched_doc.get(field, _MISSING)
    }
    unknown = changed - schema.model_fields.keys()
    if unknown:
        msg = f"Unknown fields: {', '.join(sorted(unknown))}"
        raise BadRequest(msg)
    return patched.model_dump(include=changed)

BaseGenericJsonPatchAPIView

Bases: DetailGenericView[PK]

Base view handling PATCH requests with RFC 6902 JSON Patch documents.

Source code in fastapi_views/views/jsonpatch.py
class BaseGenericJsonPatchAPIView(DetailGenericView[PK]):
    """Base view handling PATCH requests with RFC 6902 JSON Patch documents."""

    if TYPE_CHECKING:
        partial_update: Callable

    partial_update_schema: type[BaseModel]

    @classmethod
    def get_extra_annotations(cls, action: str) -> dict[str, Any]:
        if action == "partial_update":
            return {
                "pk": cls._pk_annotation(),
                "partial_update_schema": PartialUpdateSchema,
            }

        return {}

AsyncGenericJsonPatchAPIView

Bases: BaseGenericJsonPatchAPIView[PK], AsyncPartialUpdateAPIView, JsonPatchViewMixin, WithAsyncRepositoryMixin[M]

AsyncGenericJsonPatchAPIView

Source code in fastapi_views/views/jsonpatch.py
class AsyncGenericJsonPatchAPIView(
    BaseGenericJsonPatchAPIView[PK],
    AsyncPartialUpdateAPIView,
    JsonPatchViewMixin,
    WithAsyncRepositoryMixin[M],
):
    """AsyncGenericJsonPatchAPIView"""

    async def partial_update(
        self, pk: PK, partial_update_schema: JsonPatchModel
    ) -> Any:
        args, kwargs = self.get_primary_key(pk, action="partial_update")
        model = await self.repository.get(*args, **kwargs)
        if model is None:
            self.raise_not_found_error()
        data = self.apply_patch(model, partial_update_schema)
        if not data:
            return model
        await self.before_partial_update(data)
        obj = await self.repository.update_one(data, *args, **kwargs)
        if obj is None:
            self.raise_not_found_error()
        await self.after_partial_update(obj)
        return obj

    async def before_partial_update(self, data: dict[str, Any]) -> None:
        pass

    async def after_partial_update(self, model: M) -> None:
        pass

GenericJsonPatchAPIView

Bases: BaseGenericJsonPatchAPIView[PK], PartialUpdateAPIView, JsonPatchViewMixin, WithRepositoryMixin[M]

GenericJsonPatchAPIView

Source code in fastapi_views/views/jsonpatch.py
class GenericJsonPatchAPIView(
    BaseGenericJsonPatchAPIView[PK],
    PartialUpdateAPIView,
    JsonPatchViewMixin,
    WithRepositoryMixin[M],
):
    """GenericJsonPatchAPIView"""

    def partial_update(self, pk: PK, partial_update_schema: JsonPatchModel) -> Any:
        args, kwargs = self.get_primary_key(pk, action="partial_update")
        model = self.repository.get(*args, **kwargs)
        if model is None:
            self.raise_not_found_error()
        data = self.apply_patch(model, partial_update_schema)
        if not data:
            return model
        self.before_partial_update(data)
        obj = self.repository.update_one(data, *args, **kwargs)
        if obj is None:
            self.raise_not_found_error()
        self.after_partial_update(obj)
        return obj

    def before_partial_update(self, data: dict[str, Any]) -> None:
        pass

    def after_partial_update(self, model: M) -> None:
        pass

Models

JsonPatchModel

Bases: RootModel[JsonPatch]

RFC 6902 JSON Patch document.

Source code in fastapi_views/models/jsonpatch.py
class JsonPatchModel(RootModel[JsonPatch]):
    """RFC 6902 JSON Patch document."""

    __content_type__ = "application/json-patch+json"

    def apply(self, doc: Any, *, in_place: bool = False) -> Any:
        """Apply the patch operations to ``doc`` and return the patched document."""
        return apply(doc, self.root, in_place=in_place)

apply(doc: Any, *, in_place: bool = False) -> Any

Apply the patch operations to doc and return the patched document.

Source code in fastapi_views/models/jsonpatch.py
def apply(self, doc: Any, *, in_place: bool = False) -> Any:
    """Apply the patch operations to ``doc`` and return the patched document."""
    return apply(doc, self.root, in_place=in_place)

apply(doc: Any, operations: JsonPatch, *, in_place: bool = False) -> Any

Apply RFC 6902 operations to doc and return the patched document.

With in_place=False (the default) doc is left untouched and a patched copy is returned. Root-path operations always produce a new document, so use the return value rather than relying on mutation.

Source code in fastapi_views/models/jsonpatch.py
def apply(doc: Any, operations: JsonPatch, *, in_place: bool = False) -> Any:
    """Apply RFC 6902 ``operations`` to ``doc`` and return the patched document.

    With ``in_place=False`` (the default) ``doc`` is left untouched and a
    patched copy is returned. Root-path operations always produce a new
    document, so use the return value rather than relying on mutation.
    """
    patch = jsonpatch.JsonPatch(operations)
    return patch.apply(doc, in_place=in_place)