From 5ccc53c7269058a0339a1eccc3715ba8111d0401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arttu=20Per=C3=A4l=C3=A4?= Date: Tue, 7 Mar 2023 10:23:47 +0200 Subject: [PATCH 1/2] Use /data as the pointer for non-field serializer errors --- AUTHORS | 1 + CHANGELOG.md | 1 + example/api/serializers/identity.py | 7 ++++++ example/tests/test_generic_viewset.py | 32 +++++++++++++++++++++++++++ rest_framework_json_api/utils.py | 9 ++++++-- 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index e27ba5f5..797ab52f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ Adam Ziolkowski Alan Crosswell Alex Seidmann Anton Shutik +Arttu Perälä Ashley Loewen Asif Saif Uddin Beni Keller diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1c3444..3f9c175e 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 069259d2..5e2a42e3 100644 --- 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/example/tests/test_generic_viewset.py b/example/tests/test_generic_viewset.py index c8a28f24..40812d6e 100644 --- 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() diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 3d374eed..dab8a3bb 100644 --- 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): From 9808558c5a3a4fec03e3a2b400c0327efe2b9234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arttu=20Per=C3=A4l=C3=A4?= Date: Fri, 10 Mar 2023 07:41:34 +0200 Subject: [PATCH 2/2] Include NON_FIELD_ERRORS_KEY in reserved field names --- rest_framework_json_api/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index a288471e..4b619ac4 100644 --- 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()