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