Skip to content

support nested structures #776

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 22 commits into from
May 14, 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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ any parts of the framework not mentioned in the documentation should generally b

## [Unreleased]

### Added

* Added support for serializing nested serializers as attribute json value introducing setting `JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE`

### Fixed

* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`

### Changed

Expand All @@ -22,6 +26,7 @@ any parts of the framework not mentioned in the documentation should generally b
### Deprecated

* Deprecate `source` argument of `SerializerMethodResourceRelatedField`, use `method_name` instead
* Rendering nested serializers as relationships is deprecated. Use `ResourceRelatedField` instead


## [3.1.0] - 2020-02-08
Expand Down
86 changes: 85 additions & 1 deletion example/tests/unit/test_renderers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json

import pytest
from django.test import override_settings
from django.utils import timezone

from rest_framework_json_api import serializers, views
from rest_framework_json_api.renderers import JSONRenderer

from example.models import Author, Comment, Entry
from example.models import Author, Comment, Entry, Blog


# serializers
Expand Down Expand Up @@ -38,6 +40,31 @@ class JSONAPIMeta:
included_resources = ('related_models',)


class EntryDRFSerializers(serializers.ModelSerializer):

class Meta:
model = Entry
fields = ('headline', 'body_text')
read_only_fields = ('tags',)


class CommentWithNestedFieldsSerializer(serializers.ModelSerializer):
entry = EntryDRFSerializers()

class Meta:
model = Comment
exclude = ('created_at', 'modified_at', 'author')
# fields = ('entry', 'body', 'author',)


class AuthorWithNestedFieldsSerializer(serializers.ModelSerializer):
comments = CommentWithNestedFieldsSerializer(many=True)

class Meta:
model = Author
fields = ('name', 'email', 'comments')


# views
class DummyTestViewSet(views.ModelViewSet):
queryset = Entry.objects.all()
Expand All @@ -49,6 +76,12 @@ class ReadOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
serializer_class = DummyTestSerializer


class AuthorWithNestedFieldsViewSet(views.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorWithNestedFieldsSerializer
resource_name = 'authors'


def render_dummy_test_serialized_view(view_class, instance):
serializer = view_class.serializer_class(instance=instance)
renderer = JSONRenderer()
Expand Down Expand Up @@ -138,3 +171,54 @@ def test_extract_relation_instance(comment):
field=serializer.fields['blog'], resource_instance=comment
)
assert got == comment.entry.blog


def test_attribute_rendering_strategy(db):
# setting up
blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
entry = Entry.objects.create(
blog=blog,
headline='headline',
body_text='body_text',
pub_date=timezone.now(),
mod_date=timezone.now(),
n_comments=0,
n_pingbacks=0,
rating=3
)

author = Author.objects.create(name='some_author', email='[email protected]')
entry.authors.add(author)

Comment.objects.create(
entry=entry,
body='testing one two three',
author=Author.objects.first()
)

with override_settings(
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True):
rendered = render_dummy_test_serialized_view(AuthorWithNestedFieldsViewSet, author)
result = json.loads(rendered.decode())

expected = {
"data": {
"type": "authors",
"id": "1",
"attributes": {
"name": "some_author",
"email": "[email protected]",
"comments": [
{
"id": 1,
"entry": {
'headline': 'headline',
'body_text': 'body_text',
},
"body": "testing one two three"
}
]
}
}
}
assert expected == result
3 changes: 1 addition & 2 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
EntryDRFSerializers,
EntrySerializer,
ProjectSerializer,
ProjectTypeSerializer
)
ProjectTypeSerializer)

HTTP_422_UNPROCESSABLE_ENTITY = 422

Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ DJANGO_SETTINGS_MODULE=example.settings.test
filterwarnings =
error::DeprecationWarning
error::PendingDeprecationWarning
ignore::DeprecationWarning:rest_framework_json_api.serializers
19 changes: 16 additions & 3 deletions rest_framework_json_api/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rest_framework.relations import PKOnlyObject
from rest_framework.serializers import BaseSerializer, ListSerializer, Serializer
from rest_framework.settings import api_settings
from .settings import json_api_settings

import rest_framework_json_api
from rest_framework_json_api import utils
Expand Down Expand Up @@ -52,6 +53,7 @@ def extract_attributes(cls, fields, resource):
Builds the `attributes` object of the JSON API resource object.
"""
data = OrderedDict()
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE
for field_name, field in iter(fields.items()):
# ID is always provided in the root of JSON API so remove it from attributes
if field_name == 'id':
Expand All @@ -61,10 +63,13 @@ def extract_attributes(cls, fields, resource):
continue
# Skip fields with relations
if isinstance(
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
field, (relations.RelatedField, relations.ManyRelatedField)
):
continue

if isinstance(field, BaseSerializer) and not render_nested_as_attribute:
continue

# Skip read_only attribute fields when `resource` is an empty
# serializer. Prevents the "Raw Data" form of the browsable API
# from rendering `"foo": null` for read only fields
Expand All @@ -89,6 +94,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
from rest_framework_json_api.relations import ResourceRelatedField

data = OrderedDict()
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE

# Don't try to extract relationships from a non-existent resource
if resource_instance is None:
Expand All @@ -109,6 +115,9 @@ def extract_relationships(cls, fields, resource, resource_instance):
):
continue

if isinstance(field, BaseSerializer) and render_nested_as_attribute:
continue

source = field.source
relation_type = utils.get_related_resource_type(field)

Expand Down Expand Up @@ -327,18 +336,22 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
included_serializers = utils.get_included_serializers(current_serializer)
included_resources = copy.copy(included_resources)
included_resources = [inflection.underscore(value) for value in included_resources]
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE

for field_name, field in iter(fields.items()):
# Skip URL field
if field_name == api_settings.URL_FIELD_NAME:
continue

# Skip fields without relations or serialized data
# Skip fields without relations
if not isinstance(
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
):
continue

if isinstance(field, BaseSerializer) and render_nested_as_attribute:
continue

try:
included_resources.remove(field_name)
except ValueError:
Expand Down
46 changes: 43 additions & 3 deletions rest_framework_json_api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import warnings

import inflection
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.query import QuerySet
Expand All @@ -15,6 +17,8 @@
get_resource_type_from_serializer
)

from rest_framework_json_api.settings import json_api_settings


class ResourceIdentifierObjectSerializer(BaseSerializer):
default_error_messages = {
Expand Down Expand Up @@ -115,8 +119,41 @@ def validate_path(serializer_class, field_path, path):
super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs)


class SerializerMetaclass(SerializerMetaclass):

@classmethod
def _get_declared_fields(cls, bases, attrs):
fields = super()._get_declared_fields(bases, attrs)
for field_name, field in fields.items():
if isinstance(field, BaseSerializer) and \
not json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE:
clazz = '{}.{}'.format(attrs['__module__'], attrs['__qualname__'])
if isinstance(field, ListSerializer):
nested_class = type(field.child).__name__
else:
nested_class = type(field).__name__

warnings.warn(DeprecationWarning(
"Rendering nested serializer as relationship is deprecated. "
"Use `ResourceRelatedField` instead if {} in serializer {} should remain "
"a relationship. Otherwise set "
"JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE to True to render nested "
"serializer as nested json attribute".format(nested_class, clazz)))
return fields


# If user imports serializer from here we can catch class definition and check
# nested serializers for depricated use.
class Serializer(
IncludedResourcesValidationMixin, SparseFieldsetsMixin, Serializer,
metaclass=SerializerMetaclass
):
pass


class HyperlinkedModelSerializer(
IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer
IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer,
metaclass=SerializerMetaclass
):
"""
A type of `ModelSerializer` that uses hyperlinked relationships instead
Expand All @@ -132,7 +169,8 @@ class HyperlinkedModelSerializer(
"""


class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer):
class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer,
metaclass=SerializerMetaclass):
"""
A `ModelSerializer` is just a regular `Serializer`, except that:

Expand Down Expand Up @@ -193,9 +231,11 @@ def to_representation(self, instance):
def _get_field_representation(self, field, instance):
request = self.context.get('request')
is_included = field.source in get_included_resources(request)
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE
if not is_included and \
isinstance(field, ModelSerializer) and \
hasattr(instance, field.source + '_id'):
hasattr(instance, field.source + '_id') and \
not render_nested_as_attribute:
attribute = getattr(instance, field.source + '_id')

if attribute is None:
Expand Down
1 change: 1 addition & 0 deletions rest_framework_json_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'FORMAT_TYPES': False,
'PLURALIZE_TYPES': False,
'UNIFORM_EXCEPTIONS': False,
'SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE': False
}


Expand Down