Skip to content

Commit ee679ee

Browse files
authored
Use relationships in the error object pointer when the field is actually a relationship (#986)
1 parent 0816192 commit ee679ee

File tree

6 files changed

+63
-15
lines changed

6 files changed

+63
-15
lines changed

‎AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Léo S. <leo@naeka.fr>
2424
Luc Cary <luc.cary@gmail.com>
2525
Mansi Dhruv <mansi.p.dhruv@gmail.com>
2626
Matt Layman <https://www.mattlayman.com>
27+
Mehdy Khoshnoody <mehdy.khoshnoody@gmail.com>
2728
Michael Haselton <icereval@gmail.com>
2829
Mohammed Ali Zubair <mazg1493@gmail.com>
2930
Nathanael Gordon <nathanael.l.gordon@gmail.com>

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ any parts of the framework not mentioned in the documentation should generally b
1515
* Adjusted error messages to correctly use capital "JSON:API" abbreviation as used in the specification.
1616
* Avoid error when `parser_context` is `None` while parsing.
1717
* Raise comprehensible error when reserved field names `meta` and `results` are used.
18+
* Use `relationships` in the error object `pointer` when the field is actually a relationship.
1819

1920
### Changed
2021

‎example/tests/snapshots/snap_test_errors.py

+11
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@
4444
]
4545
}
4646

47+
snapshots["test_relationship_errors_has_correct_pointers 1"] = {
48+
"errors": [
49+
{
50+
"code": "incorrect_type",
51+
"detail": "Incorrect type. Expected resource identifier object, received str.",
52+
"source": {"pointer": "/data/relationships/author"},
53+
"status": "400",
54+
}
55+
]
56+
}
57+
4758
snapshots["test_second_level_array_error 1"] = {
4859
"errors": [
4960
{

‎example/tests/test_errors.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import pytest
22
from django.test import override_settings
33
from django.urls import path, reverse
4-
from rest_framework import views
4+
from rest_framework import generics
55

66
from rest_framework_json_api import serializers
77

8-
from example.models import Blog
8+
from example.models import Author, Blog
99

1010

1111
# serializers
@@ -30,6 +30,9 @@ class EntrySerializer(serializers.Serializer):
3030
comment = CommentSerializer(required=False)
3131
headline = serializers.CharField(allow_null=True, required=True)
3232
body_text = serializers.CharField()
33+
author = serializers.ResourceRelatedField(
34+
queryset=Author.objects.all(), required=False
35+
)
3336

3437
def validate(self, attrs):
3538
body_text = attrs["body_text"]
@@ -40,13 +43,12 @@ def validate(self, attrs):
4043

4144

4245
# view
43-
class DummyTestView(views.APIView):
46+
class DummyTestView(generics.CreateAPIView):
4447
serializer_class = EntrySerializer
4548
resource_name = "entries"
4649

47-
def post(self, request, *args, **kwargs):
48-
serializer = self.serializer_class(data=request.data)
49-
serializer.is_valid(raise_exception=True)
50+
def get_serializer_context(self):
51+
return {}
5052

5153

5254
urlpatterns = [
@@ -191,3 +193,21 @@ def test_many_third_level_dict_errors(client, some_blog, snapshot):
191193
}
192194

193195
snapshot.assert_match(perform_error_test(client, data))
196+
197+
198+
def test_relationship_errors_has_correct_pointers(client, some_blog, snapshot):
199+
data = {
200+
"data": {
201+
"type": "entries",
202+
"attributes": {
203+
"blog": some_blog.pk,
204+
"bodyText": "body_text",
205+
"headline": "headline",
206+
},
207+
"relationships": {
208+
"author": {"data": {"id": "INVALID_ID", "type": "authors"}}
209+
},
210+
}
211+
}
212+
213+
snapshot.assert_match(perform_error_test(client, data))

‎rest_framework_json_api/renderers.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def extract_attributes(cls, fields, resource):
6565
if fields[field_name].write_only:
6666
continue
6767
# Skip fields with relations
68-
if isinstance(field, (relations.RelatedField, relations.ManyRelatedField)):
68+
if utils.is_relationship_field(field):
6969
continue
7070

7171
# Skip read_only attribute fields when `resource` is an empty
@@ -105,9 +105,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
105105
continue
106106

107107
# Skip fields without relations
108-
if not isinstance(
109-
field, (relations.RelatedField, relations.ManyRelatedField)
110-
):
108+
if not utils.is_relationship_field(field):
111109
continue
112110

113111
source = field.source
@@ -298,9 +296,7 @@ def extract_included(
298296
continue
299297

300298
# Skip fields without relations
301-
if not isinstance(
302-
field, (relations.RelatedField, relations.ManyRelatedField)
303-
):
299+
if not utils.is_relationship_field(field):
304300
continue
305301

306302
try:

‎rest_framework_json_api/utils.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from django.http import Http404
1414
from django.utils import encoding
1515
from django.utils.translation import gettext_lazy as _
16-
from rest_framework import exceptions
16+
from rest_framework import exceptions, relations
1717
from rest_framework.exceptions import APIException
1818

1919
from .settings import json_api_settings
@@ -368,6 +368,10 @@ def get_relation_instance(resource_instance, source, serializer):
368368
return True, relation_instance
369369

370370

371+
def is_relationship_field(field):
372+
return isinstance(field, (relations.RelatedField, relations.ManyRelatedField))
373+
374+
371375
class Hyperlink(str):
372376
"""
373377
A string like object that additionally has an associated name.
@@ -394,9 +398,24 @@ def format_drf_errors(response, context, exc):
394398
errors.extend(format_error_object(message, "/data", response))
395399
# handle all errors thrown from serializers
396400
else:
401+
# Avoid circular deps
402+
from rest_framework import generics
403+
404+
has_serializer = isinstance(context["view"], generics.GenericAPIView)
405+
if has_serializer:
406+
serializer = context["view"].get_serializer()
407+
fields = get_serializer_fields(serializer) or dict()
408+
relationship_fields = [
409+
name for name, field in fields.items() if is_relationship_field(field)
410+
]
411+
397412
for field, error in response.data.items():
398413
field = format_field_name(field)
399-
pointer = "/data/attributes/{}".format(field)
414+
pointer = None
415+
# pointer can be determined only if there's a serializer.
416+
if has_serializer:
417+
rel = "relationships" if field in relationship_fields else "attributes"
418+
pointer = "/data/{}/{}".format(rel, field)
400419
if isinstance(exc, Http404) and isinstance(error, str):
401420
# 404 errors don't have a pointer
402421
errors.extend(format_error_object(error, None, response))

0 commit comments

Comments
 (0)