Repository: django-json-api/django-rest-framework-json-api

Issue:
Avoid overwriting of pointer when using custom errors
The code that produces JSON:API error objects assumes that all fields being validated are under `/data/attributes`:

https://github.com/django-json-api/django-rest-framework-json-api/blob/b0257c065ac2439036ac0543139e7f5913755177/rest_framework_json_api/utils.py#L339

and there doesn't appear to be a way to override the `pointer` value when specifying the field on a serializer.

Use case: I would like to be able to specify a serializer for the top-level `"meta"` field and have any validation errors on that data generate responses properly populated with `"source": {"pointer": "/meta/foo"}`.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91faffd..91551dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ any parts of the framework not mentioned in the documentation should generally b
 
 * Fixed invalid relationship pointer in error objects when field naming formatting is used.
 * Properly resolved related resource type when nested source field is defined.
+* Prevented overwriting of pointer in custom error object
 
 ### Added
 
diff --git a/docs/usage.md b/docs/usage.md
index 939e4e6..952fe80 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -237,7 +237,7 @@ class MyViewset(ModelViewSet):
 
 ```
 
-### Exception handling
+### Error objects / Exception handling
 
 For the `exception_handler` class, if the optional `JSON_API_UNIFORM_EXCEPTIONS` is set to True,
 all exceptions will respond with the JSON:API [error format](https://jsonapi.org/format/#error-objects).
@@ -245,6 +245,21 @@ all exceptions will respond with the JSON:API [error format](https://jsonapi.org
 When `JSON_API_UNIFORM_EXCEPTIONS` is False (the default), non-JSON:API views will respond
 with the normal DRF error format.
 
+In case you need a custom error object you can simply raise an `rest_framework.serializers.ValidationError` like the following:
+
+```python
+raise serializers.ValidationError(
+    {
+        "id": "your-id",
+        "detail": "your detail message",
+        "source": {
+            "pointer": "/data/attributes/your-pointer",
+        }
+
+    }
+)
+```
+
 ### Performance Testing
 
 If you are trying to see if your viewsets are configured properly to optimize performance,
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
index 8317c10..2e86e76 100644
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -417,21 +417,20 @@ def format_error_object(message, pointer, response):
     if isinstance(message, dict):
 
         # as there is no required field in error object we check that all fields are string
-        # except links and source which might be a dict
+        # except links, source or meta which might be a dict
         is_custom_error = all(
             [
                 isinstance(value, str)
                 for key, value in message.items()
-                if key not in ["links", "source"]
+                if key not in ["links", "source", "meta"]
             ]
         )
 
         if is_custom_error:
             if "source" not in message:
                 message["source"] = {}
-            message["source"] = {
-                "pointer": pointer,
-            }
+            if "pointer" not in message["source"]:
+                message["source"]["pointer"] = pointer
             errors.append(message)
         else:
             for k, v in message.items():


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9b47e9f..038e8ce 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,6 +7,7 @@ from rest_framework.views import APIView
 
 from rest_framework_json_api import serializers
 from rest_framework_json_api.utils import (
+    format_error_object,
     format_field_name,
     format_field_names,
     format_link_segment,
@@ -389,3 +390,45 @@ def test_get_resource_type_from_serializer_without_resource_name_raises_error():
         "can not detect 'resource_name' on serializer "
         "'SerializerWithoutResourceName' in module 'tests.test_utils'"
     )
+
+
+@pytest.mark.parametrize(
+    "message,pointer,response,result",
+    [
+        # Test that pointer does not get overridden in custom error
+        (
+            {
+                "status": "400",
+                "source": {
+                    "pointer": "/data/custom-pointer",
+                },
+                "meta": {"key": "value"},
+            },
+            "/data/default-pointer",
+            Response(status.HTTP_400_BAD_REQUEST),
+            [
+                {
+                    "status": "400",
+                    "source": {"pointer": "/data/custom-pointer"},
+                    "meta": {"key": "value"},
+                }
+            ],
+        ),
+        # Test that pointer gets added to custom error
+        (
+            {
+                "detail": "custom message",
+            },
+            "/data/default-pointer",
+            Response(status.HTTP_400_BAD_REQUEST),
+            [
+                {
+                    "detail": "custom message",
+                    "source": {"pointer": "/data/default-pointer"},
+                }
+            ],
+        ),
+    ],
+)
+def test_format_error_object(message, pointer, response, result):
+    assert result == format_error_object(message, pointer, response)