Skip to content

Commit 2adfb5b

Browse files
authored
serialize nested serializers as attribute json value (#776)
1 parent 1318610 commit 2adfb5b

File tree

7 files changed

+153
-10
lines changed

7 files changed

+153
-10
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ any parts of the framework not mentioned in the documentation should generally b
1010

1111
## [Unreleased]
1212

13+
### Added
14+
15+
* Added support for serializing nested serializers as attribute json value introducing setting `JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE`
16+
1317
### Fixed
1418

15-
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
19+
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
1620

1721
### Changed
1822

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

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

2631

2732
## [3.1.0] - 2020-02-08

example/tests/unit/test_renderers.py

+85-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
22

33
import pytest
4+
from django.test import override_settings
5+
from django.utils import timezone
46

57
from rest_framework_json_api import serializers, views
68
from rest_framework_json_api.renderers import JSONRenderer
79

8-
from example.models import Author, Comment, Entry
10+
from example.models import Author, Comment, Entry, Blog
911

1012

1113
# serializers
@@ -38,6 +40,31 @@ class JSONAPIMeta:
3840
included_resources = ('related_models',)
3941

4042

43+
class EntryDRFSerializers(serializers.ModelSerializer):
44+
45+
class Meta:
46+
model = Entry
47+
fields = ('headline', 'body_text')
48+
read_only_fields = ('tags',)
49+
50+
51+
class CommentWithNestedFieldsSerializer(serializers.ModelSerializer):
52+
entry = EntryDRFSerializers()
53+
54+
class Meta:
55+
model = Comment
56+
exclude = ('created_at', 'modified_at', 'author')
57+
# fields = ('entry', 'body', 'author',)
58+
59+
60+
class AuthorWithNestedFieldsSerializer(serializers.ModelSerializer):
61+
comments = CommentWithNestedFieldsSerializer(many=True)
62+
63+
class Meta:
64+
model = Author
65+
fields = ('name', 'email', 'comments')
66+
67+
4168
# views
4269
class DummyTestViewSet(views.ModelViewSet):
4370
queryset = Entry.objects.all()
@@ -49,6 +76,12 @@ class ReadOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
4976
serializer_class = DummyTestSerializer
5077

5178

79+
class AuthorWithNestedFieldsViewSet(views.ModelViewSet):
80+
queryset = Author.objects.all()
81+
serializer_class = AuthorWithNestedFieldsSerializer
82+
resource_name = 'authors'
83+
84+
5285
def render_dummy_test_serialized_view(view_class, instance):
5386
serializer = view_class.serializer_class(instance=instance)
5487
renderer = JSONRenderer()
@@ -138,3 +171,54 @@ def test_extract_relation_instance(comment):
138171
field=serializer.fields['blog'], resource_instance=comment
139172
)
140173
assert got == comment.entry.blog
174+
175+
176+
def test_attribute_rendering_strategy(db):
177+
# setting up
178+
blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
179+
entry = Entry.objects.create(
180+
blog=blog,
181+
headline='headline',
182+
body_text='body_text',
183+
pub_date=timezone.now(),
184+
mod_date=timezone.now(),
185+
n_comments=0,
186+
n_pingbacks=0,
187+
rating=3
188+
)
189+
190+
author = Author.objects.create(name='some_author', email='[email protected]')
191+
entry.authors.add(author)
192+
193+
Comment.objects.create(
194+
entry=entry,
195+
body='testing one two three',
196+
author=Author.objects.first()
197+
)
198+
199+
with override_settings(
200+
JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE=True):
201+
rendered = render_dummy_test_serialized_view(AuthorWithNestedFieldsViewSet, author)
202+
result = json.loads(rendered.decode())
203+
204+
expected = {
205+
"data": {
206+
"type": "authors",
207+
"id": "1",
208+
"attributes": {
209+
"name": "some_author",
210+
"email": "[email protected]",
211+
"comments": [
212+
{
213+
"id": 1,
214+
"entry": {
215+
'headline': 'headline',
216+
'body_text': 'body_text',
217+
},
218+
"body": "testing one two three"
219+
}
220+
]
221+
}
222+
}
223+
}
224+
assert expected == result

example/views.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
EntryDRFSerializers,
2424
EntrySerializer,
2525
ProjectSerializer,
26-
ProjectTypeSerializer
27-
)
26+
ProjectTypeSerializer)
2827

2928
HTTP_422_UNPROCESSABLE_ENTITY = 422
3029

pytest.ini

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ DJANGO_SETTINGS_MODULE=example.settings.test
33
filterwarnings =
44
error::DeprecationWarning
55
error::PendingDeprecationWarning
6+
ignore::DeprecationWarning:rest_framework_json_api.serializers

rest_framework_json_api/renderers.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rest_framework.relations import PKOnlyObject
1414
from rest_framework.serializers import BaseSerializer, ListSerializer, Serializer
1515
from rest_framework.settings import api_settings
16+
from .settings import json_api_settings
1617

1718
import rest_framework_json_api
1819
from rest_framework_json_api import utils
@@ -52,6 +53,7 @@ def extract_attributes(cls, fields, resource):
5253
Builds the `attributes` object of the JSON API resource object.
5354
"""
5455
data = OrderedDict()
56+
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE
5557
for field_name, field in iter(fields.items()):
5658
# ID is always provided in the root of JSON API so remove it from attributes
5759
if field_name == 'id':
@@ -61,10 +63,13 @@ def extract_attributes(cls, fields, resource):
6163
continue
6264
# Skip fields with relations
6365
if isinstance(
64-
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
66+
field, (relations.RelatedField, relations.ManyRelatedField)
6567
):
6668
continue
6769

70+
if isinstance(field, BaseSerializer) and not render_nested_as_attribute:
71+
continue
72+
6873
# Skip read_only attribute fields when `resource` is an empty
6974
# serializer. Prevents the "Raw Data" form of the browsable API
7075
# from rendering `"foo": null` for read only fields
@@ -89,6 +94,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
8994
from rest_framework_json_api.relations import ResourceRelatedField
9095

9196
data = OrderedDict()
97+
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE
9298

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

118+
if isinstance(field, BaseSerializer) and render_nested_as_attribute:
119+
continue
120+
112121
source = field.source
113122
relation_type = utils.get_related_resource_type(field)
114123

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

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

336-
# Skip fields without relations or serialized data
346+
# Skip fields without relations
337347
if not isinstance(
338-
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
348+
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
339349
):
340350
continue
341351

352+
if isinstance(field, BaseSerializer) and render_nested_as_attribute:
353+
continue
354+
342355
try:
343356
included_resources.remove(field_name)
344357
except ValueError:

rest_framework_json_api/serializers.py

+43-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import inflection
24
from django.core.exceptions import ObjectDoesNotExist
35
from django.db.models.query import QuerySet
@@ -15,6 +17,8 @@
1517
get_resource_type_from_serializer
1618
)
1719

20+
from rest_framework_json_api.settings import json_api_settings
21+
1822

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

117121

122+
class SerializerMetaclass(SerializerMetaclass):
123+
124+
@classmethod
125+
def _get_declared_fields(cls, bases, attrs):
126+
fields = super()._get_declared_fields(bases, attrs)
127+
for field_name, field in fields.items():
128+
if isinstance(field, BaseSerializer) and \
129+
not json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE:
130+
clazz = '{}.{}'.format(attrs['__module__'], attrs['__qualname__'])
131+
if isinstance(field, ListSerializer):
132+
nested_class = type(field.child).__name__
133+
else:
134+
nested_class = type(field).__name__
135+
136+
warnings.warn(DeprecationWarning(
137+
"Rendering nested serializer as relationship is deprecated. "
138+
"Use `ResourceRelatedField` instead if {} in serializer {} should remain "
139+
"a relationship. Otherwise set "
140+
"JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE to True to render nested "
141+
"serializer as nested json attribute".format(nested_class, clazz)))
142+
return fields
143+
144+
145+
# If user imports serializer from here we can catch class definition and check
146+
# nested serializers for depricated use.
147+
class Serializer(
148+
IncludedResourcesValidationMixin, SparseFieldsetsMixin, Serializer,
149+
metaclass=SerializerMetaclass
150+
):
151+
pass
152+
153+
118154
class HyperlinkedModelSerializer(
119-
IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer
155+
IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer,
156+
metaclass=SerializerMetaclass
120157
):
121158
"""
122159
A type of `ModelSerializer` that uses hyperlinked relationships instead
@@ -132,7 +169,8 @@ class HyperlinkedModelSerializer(
132169
"""
133170

134171

135-
class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer):
172+
class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer,
173+
metaclass=SerializerMetaclass):
136174
"""
137175
A `ModelSerializer` is just a regular `Serializer`, except that:
138176
@@ -193,9 +231,11 @@ def to_representation(self, instance):
193231
def _get_field_representation(self, field, instance):
194232
request = self.context.get('request')
195233
is_included = field.source in get_included_resources(request)
234+
render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE
196235
if not is_included and \
197236
isinstance(field, ModelSerializer) and \
198-
hasattr(instance, field.source + '_id'):
237+
hasattr(instance, field.source + '_id') and \
238+
not render_nested_as_attribute:
199239
attribute = getattr(instance, field.source + '_id')
200240

201241
if attribute is None:

rest_framework_json_api/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
'FORMAT_TYPES': False,
1515
'PLURALIZE_TYPES': False,
1616
'UNIFORM_EXCEPTIONS': False,
17+
'SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE': False
1718
}
1819

1920

0 commit comments

Comments
 (0)