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

Issue:
Object-level validation errors given an incorrect source.pointer
## Description of the Bug Report

When raising a non-field `ValidationError` in a serializer [in accordance with the DRF documentation](https://www.django-rest-framework.org/api-guide/serializers/#object-level-validation), the error is attributed to a non-existing `/data/attributes/non_field_errors` path instead of `/data`.

```python
from rest_framework_json_api import serializers

class ProductSerializer(serializers.ModelSerializer):
    def validate(self, data):
        if data.get("best_before_date") and data.get("expiry_date"):
            raise serializers.ValidationError(
                "Specify either a best before date or an expiry date."
            )
        return data
```

```json
{
    "errors": [
        {
            "detail": "Specify either a best before date or an expiry date.",
            "status": "400",
            "source": {
                "pointer": "/data/attributes/non_field_errors"
            },
            "code": "invalid"
        }
    ]
}
```

Complete minimal example available [here](https://github.com/arttuperala/jsonapi_non_field_errors).

### Discussed in https://github.com/django-json-api/django-rest-framework-json-api/discussions/1135

## Checklist

- [x] Certain that this is a bug (if unsure or you have a question use [discussions](https://github.com/django-json-api/django-rest-framework-json-api/discussions) instead)
- [x] Code snippet or unit test added to reproduce bug


Comments:

Patch:
diff --git a/AUTHORS b/AUTHORS
index e27ba5f..797ab52 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -3,6 +3,7 @@ Adam Ziolkowski <adam@adsized.com>
 Alan Crosswell <alan@columbia.edu>
 Alex Seidmann <alex@leanpnt.de>
 Anton Shutik <shutikanton@gmail.com>
+Arttu Perälä <arttu@perala.me>
 Ashley Loewen <github@ashleycodes.tech>
 Asif Saif Uddin <auvipy@gmail.com>
 Beni Keller <beni@matraxi.ch>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a1c344..3f9c175 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ any parts of the framework not mentioned in the documentation should generally b
 ### Fixed
 
 * Refactored handling of the `sort` query parameter to fix duplicate declaration in the generated schema definition
+* Non-field serializer errors are given a source.pointer value of "/data".
 
 ## [6.0.0] - 2022-09-24
 
diff --git a/example/api/serializers/identity.py b/example/api/serializers/identity.py
index 069259d..5e2a42e 100644
--- a/example/api/serializers/identity.py
+++ b/example/api/serializers/identity.py
@@ -23,6 +23,13 @@ class IdentitySerializer(serializers.ModelSerializer):
             )
         return data
 
+    def validate(self, data):
+        if data["first_name"] == data["last_name"]:
+            raise serializers.ValidationError(
+                "First name cannot be the same as last name!"
+            )
+        return data
+
     class Meta:
         model = auth_models.User
         fields = (
diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py
index a288471..4b619ac 100644
--- a/rest_framework_json_api/serializers.py
+++ b/rest_framework_json_api/serializers.py
@@ -155,7 +155,12 @@ class IncludedResourcesValidationMixin:
 class ReservedFieldNamesMixin:
     """Ensures that reserved field names are not used and an error raised instead."""
 
-    _reserved_field_names = {"meta", "results", "type"}
+    _reserved_field_names = {
+        "meta",
+        "results",
+        "type",
+        api_settings.NON_FIELD_ERRORS_KEY,
+    }
 
     def get_fields(self):
         fields = super().get_fields()
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
index 3d374ee..dab8a3b 100644
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -13,6 +13,7 @@ from django.utils import encoding
 from django.utils.translation import gettext_lazy as _
 from rest_framework import exceptions, relations
 from rest_framework.exceptions import APIException
+from rest_framework.settings import api_settings
 
 from .settings import json_api_settings
 
@@ -381,10 +382,14 @@ def format_drf_errors(response, context, exc):
             ]
 
         for field, error in response.data.items():
+            non_field_error = field == api_settings.NON_FIELD_ERRORS_KEY
             field = format_field_name(field)
             pointer = None
-            # pointer can be determined only if there's a serializer.
-            if has_serializer:
+            if non_field_error:
+                # Serializer error does not refer to a specific field.
+                pointer = "/data"
+            elif has_serializer:
+                # pointer can be determined only if there's a serializer.
                 rel = "relationships" if field in relationship_fields else "attributes"
                 pointer = f"/data/{rel}/{field}"
             if isinstance(exc, Http404) and isinstance(error, str):


Test patch:
diff --git a/example/tests/test_generic_viewset.py b/example/tests/test_generic_viewset.py
index c8a28f2..40812d6 100644
--- a/example/tests/test_generic_viewset.py
+++ b/example/tests/test_generic_viewset.py
@@ -129,3 +129,35 @@ class GenericViewSet(TestBase):
         )
 
         assert expected == response.json()
+
+    def test_nonfield_validation_exceptions(self):
+        """
+        Non-field errors should be attributed to /data source.pointer.
+        """
+        expected = {
+            "errors": [
+                {
+                    "status": "400",
+                    "source": {
+                        "pointer": "/data",
+                    },
+                    "detail": "First name cannot be the same as last name!",
+                    "code": "invalid",
+                },
+            ]
+        }
+        response = self.client.post(
+            "/identities",
+            {
+                "data": {
+                    "type": "users",
+                    "attributes": {
+                        "email": "miles@example.com",
+                        "first_name": "Miles",
+                        "last_name": "Miles",
+                    },
+                }
+            },
+        )
+
+        assert expected == response.json()