Skip to content

Add handling of nested errors #815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ any parts of the framework not mentioned in the documentation should generally b
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
* Clear many-to-many relationships instead of deleting related objects during PATCH on `RelationshipView`
* Allow POST, PATCH, DELETE for actions in `ReadOnlyModelViewSet`. It was problematic since 2.8.0.
* Properly format nested errors

### Changed

Expand Down
Empty file.
121 changes: 121 additions & 0 deletions example/tests/snapshots/snap_test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals

from snapshottest import Snapshot


snapshots = Snapshot()

snapshots['test_first_level_attribute_error 1'] = {
'errors': [
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/headline'
},
'status': '400'
}
]
}

snapshots['test_first_level_custom_attribute_error 1'] = {
'errors': [
{
'detail': 'Too short',
'source': {
'pointer': '/data/attributes/body-text'
},
'title': 'Too Short title'
}
]
}

snapshots['test_second_level_array_error 1'] = {
'errors': [
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/comments/0/body'
},
'status': '400'
}
]
}

snapshots['test_second_level_dict_error 1'] = {
'errors': [
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/comment/body'
},
'status': '400'
}
]
}

snapshots['test_third_level_array_error 1'] = {
'errors': [
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/comments/0/attachments/0/data'
},
'status': '400'
}
]
}

snapshots['test_third_level_custom_array_error 1'] = {
'errors': [
{
'code': 'invalid',
'detail': 'Too short data',
'source': {
'pointer': '/data/attributes/comments/0/attachments/0/data'
},
'status': '400'
}
]
}

snapshots['test_third_level_dict_error 1'] = {
'errors': [
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/comments/0/attachment/data'
},
'status': '400'
}
]
}

snapshots['test_many_third_level_dict_errors 1'] = {
'errors': [
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/comments/0/attachment/data'
},
'status': '400'
},
{
'code': 'required',
'detail': 'This field is required.',
'source': {
'pointer': '/data/attributes/comments/0/body'
},
'status': '400'
}
]
}

snapshots['test_deprecation_warning 1'] = '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'
240 changes: 240 additions & 0 deletions example/tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import pytest
from django.conf.urls import url
from django.test import override_settings
from django.urls import reverse
from rest_framework import views

from rest_framework_json_api import serializers

from example.models import Blog


# serializers
class CommentAttachmentSerializer(serializers.Serializer):
data = serializers.CharField(allow_null=False, required=True)

def validate_data(self, value):
if value and len(value) < 10:
raise serializers.ValidationError('Too short data')


class CommentSerializer(serializers.Serializer):
attachments = CommentAttachmentSerializer(many=True, required=False)
attachment = CommentAttachmentSerializer(required=False)
one_more_attachment = CommentAttachmentSerializer(required=False)
body = serializers.CharField(allow_null=False, required=True)


class EntrySerializer(serializers.Serializer):
blog = serializers.IntegerField()
comments = CommentSerializer(many=True, required=False)
comment = CommentSerializer(required=False)
headline = serializers.CharField(allow_null=True, required=True)
body_text = serializers.CharField()

def validate(self, attrs):
body_text = attrs['body_text']
if len(body_text) < 5:
raise serializers.ValidationError({'body_text': {
'title': 'Too Short title', 'detail': 'Too short'}
})


# view
class DummyTestView(views.APIView):
serializer_class = EntrySerializer
resource_name = 'entries'

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)


urlpatterns = [
url('entries-nested', DummyTestView.as_view(),
name='entries-nested-list')
]


@pytest.fixture(scope='function')
def some_blog(db):
return Blog.objects.create(name='Some Blog', tagline="It's a blog")


def perform_error_test(client, data):
with override_settings(
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True,
ROOT_URLCONF=__name__
):
url = reverse('entries-nested-list')
response = client.post(url, data=data)

return response.json()


def test_first_level_attribute_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
}
}
}
snapshot.assert_match(perform_error_test(client, data))


def test_first_level_custom_attribute_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'body-text': 'body',
'headline': 'headline'
}
}
}
with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
snapshot.assert_match(perform_error_test(client, data))


def test_second_level_array_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
'headline': 'headline',
'comments': [
{
}
]
}
}
}

snapshot.assert_match(perform_error_test(client, data))


def test_second_level_dict_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
'headline': 'headline',
'comment': {}
}
}
}

snapshot.assert_match(perform_error_test(client, data))


def test_third_level_array_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
'headline': 'headline',
'comments': [
{
'body': 'test comment',
'attachments': [
{
}
]
}
]
}
}
}

snapshot.assert_match(perform_error_test(client, data))


def test_third_level_custom_array_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
'headline': 'headline',
'comments': [
{
'body': 'test comment',
'attachments': [
{
'data': 'text'
}
]
}
]
}
}
}

snapshot.assert_match(perform_error_test(client, data))


def test_third_level_dict_error(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
'headline': 'headline',
'comments': [
{
'body': 'test comment',
'attachment': {}
}
]
}
}
}

snapshot.assert_match(perform_error_test(client, data))


def test_many_third_level_dict_errors(client, some_blog, snapshot):
data = {
'data': {
'type': 'entries',
'attributes': {
'blog': some_blog.pk,
'bodyText': 'body_text',
'headline': 'headline',
'comments': [
{
'attachment': {}
}
]
}
}
}

snapshot.assert_match(perform_error_test(client, data))


@pytest.mark.filterwarnings('default::DeprecationWarning:rest_framework_json_api.serializers')
def test_deprecation_warning(recwarn, settings, snapshot):
settings.JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE = False

class DummyNestedSerializer(serializers.Serializer):
field = serializers.CharField()

class DummySerializer(serializers.Serializer):
nested = DummyNestedSerializer(many=True)

assert len(recwarn) == 1
warning = recwarn.pop(DeprecationWarning)
snapshot.assert_match(str(warning.message))
11 changes: 6 additions & 5 deletions example/tests/test_generic_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,6 @@ def test_custom_validation_exceptions(self):
"""
expected = {
'errors': [
{
'id': 'armageddon101',
'detail': 'Hey! You need a last name!',
'meta': 'something',
},
{
'status': '400',
'source': {
Expand All @@ -108,6 +103,12 @@ def test_custom_validation_exceptions(self):
'detail': 'Enter a valid email address.',
'code': 'invalid',
},
{
'id': 'armageddon101',
'detail': 'Hey! You need a last name!',
'meta': 'something',
'source': {'pointer': '/data/attributes/lastName'}
},
]
}
response = self.client.post('/identities', {
Expand Down
Loading