Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 190afdf

Browse files
committed
Add handling of nested errors
1 parent 3eaf07c commit 190afdf

File tree

6 files changed

+401
-26
lines changed

6 files changed

+401
-26
lines changed

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ any parts of the framework not mentioned in the documentation should generally b
1919
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
2020
* Clear many-to-many relationships instead of deleting related objects during PATCH on `RelationshipView`
2121
* Allow POST, PATCH, DELETE for actions in `ReadOnlyModelViewSet`. It was problematic since 2.8.0.
22+
* Properly format nested errors
2223

2324
### Changed
2425

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# name: test_deprecation_warning
2+
DeprecationWarning('Rendering nested serializer as relationship is deprecated. Use `ResourceRelatedField` instead if DummyNestedSerializer in serializer example.tests.test_errors.test_deprecation_warning.<locals>.DummySerializer should remain a relationship. Otherwise set JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE to True to render nested serializer as nested json attribute')
3+
---
4+
# name: test_first_level_attribute_error
5+
<class 'list'> [
6+
<class 'dict'> {
7+
'code': 'required',
8+
'detail': ErrorDetail(string='This field is required.', code='required'),
9+
'source': <class 'dict'> {
10+
'pointer': '/data/attributes/headline',
11+
},
12+
'status': '400',
13+
},
14+
]
15+
---
16+
# name: test_first_level_custom_attribute_error
17+
<class 'list'> [
18+
<class 'dict'> {
19+
'detail': ErrorDetail(string='Too short', code='invalid'),
20+
'source': <class 'dict'> {
21+
'pointer': '/data/attributes/body-text',
22+
},
23+
'title': ErrorDetail(string='Too Short title', code='invalid'),
24+
},
25+
]
26+
---
27+
# name: test_many_third_level_dict_errors
28+
<class 'list'> [
29+
<class 'dict'> {
30+
'code': 'required',
31+
'detail': ErrorDetail(string='This field is required.', code='required'),
32+
'source': <class 'dict'> {
33+
'pointer': '/data/attributes/comments/0/attachment/data',
34+
},
35+
'status': '400',
36+
},
37+
<class 'dict'> {
38+
'code': 'required',
39+
'detail': ErrorDetail(string='This field is required.', code='required'),
40+
'source': <class 'dict'> {
41+
'pointer': '/data/attributes/comments/0/body',
42+
},
43+
'status': '400',
44+
},
45+
]
46+
---
47+
# name: test_second_level_array_error
48+
<class 'list'> [
49+
<class 'dict'> {
50+
'code': 'required',
51+
'detail': ErrorDetail(string='This field is required.', code='required'),
52+
'source': <class 'dict'> {
53+
'pointer': '/data/attributes/comments/0/body',
54+
},
55+
'status': '400',
56+
},
57+
]
58+
---
59+
# name: test_second_level_dict_error
60+
<class 'list'> [
61+
<class 'dict'> {
62+
'code': 'required',
63+
'detail': ErrorDetail(string='This field is required.', code='required'),
64+
'source': <class 'dict'> {
65+
'pointer': '/data/attributes/comment/body',
66+
},
67+
'status': '400',
68+
},
69+
]
70+
---
71+
# name: test_third_level_array_error
72+
<class 'list'> [
73+
<class 'dict'> {
74+
'code': 'required',
75+
'detail': ErrorDetail(string='This field is required.', code='required'),
76+
'source': <class 'dict'> {
77+
'pointer': '/data/attributes/comments/0/attachments/0/data',
78+
},
79+
'status': '400',
80+
},
81+
]
82+
---
83+
# name: test_third_level_custom_array_error
84+
<class 'list'> [
85+
<class 'dict'> {
86+
'code': 'invalid',
87+
'detail': ErrorDetail(string='Too short data', code='invalid'),
88+
'source': <class 'dict'> {
89+
'pointer': '/data/attributes/comments/0/attachments/0/data',
90+
},
91+
'status': '400',
92+
},
93+
]
94+
---
95+
# name: test_third_level_dict_error
96+
<class 'list'> [
97+
<class 'dict'> {
98+
'code': 'required',
99+
'detail': ErrorDetail(string='This field is required.', code='required'),
100+
'source': <class 'dict'> {
101+
'pointer': '/data/attributes/comments/0/attachment/data',
102+
},
103+
'status': '400',
104+
},
105+
]
106+
---

‎example/tests/test_errors.py‎

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import pytest
2+
from django.test import override_settings
3+
from django.urls import path, reverse
4+
from rest_framework import views
5+
6+
from rest_framework_json_api import serializers
7+
8+
from example.models import Blog
9+
10+
11+
# serializers
12+
class CommentAttachmentSerializer(serializers.Serializer):
13+
data = serializers.CharField(allow_null=False, required=True)
14+
15+
def validate_data(self, value):
16+
if value and len(value) < 10:
17+
raise serializers.ValidationError('Too short data')
18+
19+
20+
class CommentSerializer(serializers.Serializer):
21+
attachments = CommentAttachmentSerializer(many=True, required=False)
22+
attachment = CommentAttachmentSerializer(required=False)
23+
one_more_attachment = CommentAttachmentSerializer(required=False)
24+
body = serializers.CharField(allow_null=False, required=True)
25+
26+
27+
class EntrySerializer(serializers.Serializer):
28+
blog = serializers.IntegerField()
29+
comments = CommentSerializer(many=True, required=False)
30+
comment = CommentSerializer(required=False)
31+
headline = serializers.CharField(allow_null=True, required=True)
32+
body_text = serializers.CharField()
33+
34+
def validate(self, attrs):
35+
body_text = attrs['body_text']
36+
if len(body_text) < 5:
37+
raise serializers.ValidationError({'body_text': {
38+
'title': 'Too Short title', 'detail': 'Too short'}
39+
})
40+
41+
42+
# view
43+
class DummyTestView(views.APIView):
44+
serializer_class = EntrySerializer
45+
resource_name = 'entries'
46+
47+
def post(self, request, *args, **kwargs):
48+
serializer = self.serializer_class(data=request.data)
49+
serializer.is_valid(raise_exception=True)
50+
51+
52+
urlpatterns = [
53+
path('entries-nested', DummyTestView.as_view(),
54+
name='entries-nested-list')
55+
]
56+
57+
58+
@pytest.fixture(scope='function')
59+
def some_blog(db):
60+
return Blog.objects.create(name='Some Blog', tagline="It's a blog")
61+
62+
63+
def perform_error_test(client, data):
64+
with override_settings(
65+
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True,
66+
ROOT_URLCONF=__name__
67+
):
68+
url = reverse('entries-nested-list')
69+
response = client.post(url, data=data)
70+
71+
errors = response.data
72+
return errors
73+
74+
75+
def test_first_level_attribute_error(client, some_blog, snapshot):
76+
data = {
77+
'data': {
78+
'type': 'entries',
79+
'attributes': {
80+
'blog': some_blog.pk,
81+
'bodyText': 'body_text',
82+
}
83+
}
84+
}
85+
assert snapshot == perform_error_test(client, data)
86+
87+
88+
def test_first_level_custom_attribute_error(client, some_blog, snapshot):
89+
data = {
90+
'data': {
91+
'type': 'entries',
92+
'attributes': {
93+
'blog': some_blog.pk,
94+
'body-text': 'body',
95+
'headline': 'headline'
96+
}
97+
}
98+
}
99+
with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
100+
assert snapshot == perform_error_test(client, data)
101+
102+
103+
def test_second_level_array_error(client, some_blog, snapshot):
104+
data = {
105+
'data': {
106+
'type': 'entries',
107+
'attributes': {
108+
'blog': some_blog.pk,
109+
'bodyText': 'body_text',
110+
'headline': 'headline',
111+
'comments': [
112+
{
113+
}
114+
]
115+
}
116+
}
117+
}
118+
119+
assert snapshot == perform_error_test(client, data)
120+
121+
122+
def test_second_level_dict_error(client, some_blog, snapshot):
123+
data = {
124+
'data': {
125+
'type': 'entries',
126+
'attributes': {
127+
'blog': some_blog.pk,
128+
'bodyText': 'body_text',
129+
'headline': 'headline',
130+
'comment': {}
131+
}
132+
}
133+
}
134+
135+
assert snapshot == perform_error_test(client, data)
136+
137+
138+
def test_third_level_array_error(client, some_blog, snapshot):
139+
data = {
140+
'data': {
141+
'type': 'entries',
142+
'attributes': {
143+
'blog': some_blog.pk,
144+
'bodyText': 'body_text',
145+
'headline': 'headline',
146+
'comments': [
147+
{
148+
'body': 'test comment',
149+
'attachments': [
150+
{
151+
}
152+
]
153+
}
154+
]
155+
}
156+
}
157+
}
158+
159+
assert snapshot == perform_error_test(client, data)
160+
161+
162+
def test_third_level_custom_array_error(client, some_blog, snapshot):
163+
data = {
164+
'data': {
165+
'type': 'entries',
166+
'attributes': {
167+
'blog': some_blog.pk,
168+
'bodyText': 'body_text',
169+
'headline': 'headline',
170+
'comments': [
171+
{
172+
'body': 'test comment',
173+
'attachments': [
174+
{
175+
'data': 'text'
176+
}
177+
]
178+
}
179+
]
180+
}
181+
}
182+
}
183+
184+
assert snapshot == perform_error_test(client, data)
185+
186+
187+
def test_third_level_dict_error(client, some_blog, snapshot):
188+
data = {
189+
'data': {
190+
'type': 'entries',
191+
'attributes': {
192+
'blog': some_blog.pk,
193+
'bodyText': 'body_text',
194+
'headline': 'headline',
195+
'comments': [
196+
{
197+
'body': 'test comment',
198+
'attachment': {}
199+
}
200+
]
201+
}
202+
}
203+
}
204+
205+
assert snapshot == perform_error_test(client, data)
206+
207+
208+
def test_many_third_level_dict_errors(client, some_blog, snapshot):
209+
data = {
210+
'data': {
211+
'type': 'entries',
212+
'attributes': {
213+
'blog': some_blog.pk,
214+
'bodyText': 'body_text',
215+
'headline': 'headline',
216+
'comments': [
217+
{
218+
'attachment': {}
219+
}
220+
]
221+
}
222+
}
223+
}
224+
225+
assert snapshot == perform_error_test(client, data)
226+
227+
228+
def test_deprecation_warning(recwarn, settings, snapshot):
229+
settings.JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE = False
230+
231+
class DummyNestedSerializer(serializers.Serializer):
232+
field = serializers.CharField()
233+
234+
class DummySerializer(serializers.Serializer):
235+
nested = DummyNestedSerializer(many=True)
236+
237+
assert len(recwarn) == 1
238+
warning = recwarn.pop(DeprecationWarning)
239+
assert snapshot == warning.message

‎example/tests/test_generic_viewset.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,6 @@ def test_custom_validation_exceptions(self):
9595
"""
9696
expected = {
9797
'errors': [
98-
{
99-
'id': 'armageddon101',
100-
'detail': 'Hey! You need a last name!',
101-
'meta': 'something',
102-
},
10398
{
10499
'status': '400',
105100
'source': {
@@ -108,6 +103,12 @@ def test_custom_validation_exceptions(self):
108103
'detail': 'Enter a valid email address.',
109104
'code': 'invalid',
110105
},
106+
{
107+
'id': 'armageddon101',
108+
'detail': 'Hey! You need a last name!',
109+
'meta': 'something',
110+
'source': {'pointer': '/data/attributes/lastName'}
111+
},
111112
]
112113
}
113114
response = self.client.post('/identities', {

‎requirements/requirements-testing.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pytest==6.0.1
55
pytest-cov==2.10.1
66
pytest-django==3.9.0
77
pytest-factoryboy==2.0.3
8+
syrupy==0.6.1

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /