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



Discussion:

Resolving patch:
diff --git a/example/api/serializers/identity.py b/example/api/serializers/identity.py
--- a/example/api/serializers/identity.py
+++ b/example/api/serializers/identity.py
@@ -23,6 +23,13 @@ def validate_last_name(self, data):
             )
         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
--- a/rest_framework_json_api/serializers.py
+++ b/rest_framework_json_api/serializers.py
@@ -155,7 +155,12 @@ def validate_path(serializer_class, field_path, path):
 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
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -13,6 +13,7 @@
 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
--- a/example/tests/test_generic_viewset.py
+++ b/example/tests/test_generic_viewset.py
@@ -129,3 +129,35 @@ def test_custom_validation_exceptions(self):
         )
 
         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()