Skip to content

Commit 67442ab

Browse files
author
Boris Pleshakov
committed
f rewrite complex drf errors structures into JSON-API format
1 parent 65f9b15 commit 67442ab

File tree

2 files changed

+239
-18
lines changed

2 files changed

+239
-18
lines changed

example/tests/test_nested_errors.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import json
2+
3+
import pytest
4+
from django.conf.urls import url
5+
from django.test import override_settings
6+
from django.urls import reverse
7+
8+
# from example import urls_test
9+
from example.models import Blog
10+
from example.tests import TestBase
11+
from rest_framework_json_api import serializers
12+
from rest_framework import views
13+
14+
15+
# serializers
16+
class CommentAttachmentSerializer(serializers.Serializer):
17+
data = serializers.CharField(allow_null=False, required=True)
18+
19+
def validate_data(self, value):
20+
if value and len(value) < 10:
21+
raise serializers.ValidationError('Too short data')
22+
23+
24+
class CommentSerializer(serializers.Serializer):
25+
attachments = CommentAttachmentSerializer(many=True, required=False)
26+
attachment = CommentAttachmentSerializer(required=False)
27+
body = serializers.CharField(allow_null=False, required=True)
28+
29+
30+
class EntrySerializer(serializers.Serializer):
31+
blog = serializers.IntegerField()
32+
comments = CommentSerializer(many=True, required=False)
33+
comment = CommentSerializer(required=False)
34+
headline = serializers.CharField(allow_null=True, required=True)
35+
body_text = serializers.CharField()
36+
37+
def validate(self, attrs):
38+
body_text = attrs['body_text']
39+
if len(body_text) < 5:
40+
raise serializers.ValidationError({'body_text': 'Too short'})
41+
42+
43+
# view
44+
class DummyTestView(views.APIView):
45+
serializer_class = EntrySerializer
46+
resource_name = 'entries'
47+
48+
def post(self, request, *args, **kwargs):
49+
serializer = self.serializer_class(data=request.data)
50+
serializer.is_valid(raise_exception=True)
51+
52+
53+
urlpatterns = [
54+
url(r'^entries-nested/$', DummyTestView.as_view(),
55+
name='entries-nested-list')
56+
]
57+
58+
59+
@override_settings(ROOT_URLCONF=__name__)
60+
@pytest.mark.filterwarnings('ignore:Rendering nested')
61+
class TestNestedErrors(TestBase):
62+
63+
def setUp(self):
64+
super(TestNestedErrors, self).setUp()
65+
self.url = reverse('entries-nested-list')
66+
self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
67+
68+
def perform_error_test(self, data, expected_pointer):
69+
with override_settings(
70+
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True):
71+
response = self.client.post(self.url, data=data)
72+
73+
errors = response.data
74+
75+
assert len(errors) == 1
76+
assert errors[0]['source']['pointer'] == expected_pointer
77+
78+
def test_first_level_attribute_error(self):
79+
data = {
80+
'data': {
81+
'type': 'entries',
82+
'attributes': {
83+
'blog': self.blog.pk,
84+
'body_text': 'body_text',
85+
}
86+
}
87+
}
88+
self.perform_error_test(data, '/data/attributes/headline')
89+
90+
def test_first_level_custom_attribute_error(self):
91+
data = {
92+
'data': {
93+
'type': 'entries',
94+
'attributes': {
95+
'blog': self.blog.pk,
96+
'body_text': 'body',
97+
'headline': 'headline'
98+
}
99+
}
100+
}
101+
with override_settings(JSON_API_FORMAT_FIELD_NAMES='underscore'):
102+
self.perform_error_test(data, '/data/attributes/body_text')
103+
104+
def test_second_level_array_error(self):
105+
data = {
106+
'data': {
107+
'type': 'entries',
108+
'attributes': {
109+
'blog': self.blog.pk,
110+
'body_text': 'body_text',
111+
'headline': 'headline',
112+
'comments': [
113+
{
114+
}
115+
]
116+
}
117+
}
118+
}
119+
120+
self.perform_error_test(data, '/data/attributes/comments/0/body')
121+
122+
def test_second_level_dict_error(self):
123+
data = {
124+
'data': {
125+
'type': 'entries',
126+
'attributes': {
127+
'blog': self.blog.pk,
128+
'body_text': 'body_text',
129+
'headline': 'headline',
130+
'comment': {}
131+
}
132+
}
133+
}
134+
135+
self.perform_error_test(data, '/data/attributes/comment/body')
136+
137+
def test_third_level_array_error(self):
138+
data = {
139+
'data': {
140+
'type': 'entries',
141+
'attributes': {
142+
'blog': self.blog.pk,
143+
'body_text': 'body_text',
144+
'headline': 'headline',
145+
'comments': [
146+
{
147+
'body': 'test comment',
148+
'attachments': [
149+
{
150+
}
151+
]
152+
}
153+
]
154+
}
155+
}
156+
}
157+
158+
self.perform_error_test(data, '/data/attributes/comments/0/attachments/0/data')
159+
160+
def test_third_level_custom_array_error(self):
161+
data = {
162+
'data': {
163+
'type': 'entries',
164+
'attributes': {
165+
'blog': self.blog.pk,
166+
'body_text': 'body_text',
167+
'headline': 'headline',
168+
'comments': [
169+
{
170+
'body': 'test comment',
171+
'attachments': [
172+
{
173+
'data': 'text'
174+
}
175+
]
176+
}
177+
]
178+
}
179+
}
180+
}
181+
182+
self.perform_error_test(data, '/data/attributes/comments/0/attachments/0/data')
183+
184+
def test_third_level_dict_error(self):
185+
data = {
186+
'data': {
187+
'type': 'entries',
188+
'attributes': {
189+
'blog': self.blog.pk,
190+
'body_text': 'body_text',
191+
'headline': 'headline',
192+
'comments': [
193+
{
194+
'body': 'test comment',
195+
'attachment': {}
196+
}
197+
]
198+
}
199+
}
200+
}
201+
202+
self.perform_error_test(data, '/data/attributes/comments/0/attachment/data')

rest_framework_json_api/utils.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -312,29 +312,34 @@ def format_drf_errors(response, context, exc):
312312
# handle generic errors. ValidationError('test') in a view for example
313313
if isinstance(response.data, list):
314314
for message in response.data:
315-
errors.append(format_error_object(message, '/data', response))
315+
errors.extend(format_error_object(message, '/data', response))
316316
# handle all errors thrown from serializers
317317
else:
318318
for field, error in response.data.items():
319319
field = format_value(field)
320320
pointer = '/data/attributes/{}'.format(field)
321321
# see if they passed a dictionary to ValidationError manually
322+
# The bit tricky problem is here. It is may be nested drf thing in format
323+
# name: error_object, or it may be custom error thrown by user. I guess,
324+
# if it is drf error, dict will always have single key
322325
if isinstance(error, dict):
323-
errors.append(error)
326+
if len(error) > 1:
327+
errors.append(error)
328+
else:
329+
errors.extend(format_error_object(error, pointer, response))
324330
elif isinstance(exc, Http404) and isinstance(error, str):
325331
# 404 errors don't have a pointer
326-
errors.append(format_error_object(error, None, response))
332+
errors.extend(format_error_object(error, None, response))
327333
elif isinstance(error, str):
328334
classes = inspect.getmembers(exceptions, inspect.isclass)
329335
# DRF sets the `field` to 'detail' for its own exceptions
330336
if isinstance(exc, tuple(x[1] for x in classes)):
331337
pointer = '/data'
332-
errors.append(format_error_object(error, pointer, response))
338+
errors.extend(format_error_object(error, pointer, response))
333339
elif isinstance(error, list):
334-
for message in error:
335-
errors.append(format_error_object(message, pointer, response))
340+
errors.extend(format_error_object(error, pointer, response))
336341
else:
337-
errors.append(format_error_object(error, pointer, response))
342+
errors.extend(format_error_object(error, pointer, response))
338343

339344
context['view'].resource_name = 'errors'
340345
response.data = errors
@@ -343,18 +348,32 @@ def format_drf_errors(response, context, exc):
343348

344349

345350
def format_error_object(message, pointer, response):
346-
error_obj = {
347-
'detail': message,
348-
'status': encoding.force_str(response.status_code),
349-
}
350-
if pointer is not None:
351-
error_obj['source'] = {
352-
'pointer': pointer,
351+
errors = []
352+
if isinstance(message, dict):
353+
for k, v in message.items():
354+
errors.extend(format_error_object(v, pointer + '/{}'.format(k), response))
355+
elif isinstance(message, list):
356+
for num, error in enumerate(message):
357+
if isinstance(error, (list, dict)):
358+
new_pointer = pointer + '/{}'.format(num)
359+
else:
360+
new_pointer = pointer
361+
if error:
362+
errors.extend(format_error_object(error, new_pointer, response))
363+
else:
364+
error_obj = {
365+
'detail': message,
366+
'status': encoding.force_str(response.status_code),
353367
}
354-
code = getattr(message, "code", None)
355-
if code is not None:
356-
error_obj['code'] = code
357-
return error_obj
368+
if pointer is not None:
369+
error_obj['source'] = {
370+
'pointer': pointer,
371+
}
372+
code = getattr(message, "code", None)
373+
if code is not None:
374+
error_obj['code'] = code
375+
errors.append(error_obj)
376+
return errors
358377

359378

360379
def format_errors(data):

0 commit comments

Comments
 (0)