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 55cd1ea

Browse files
author
Oliver Sauder
committed
Render empty resource linkages as specified
* null for empty to-one relationships. * an empty array ([]) for empty to-many relationships. See https://jsonapi.org/format/#document-resource-object-linkage
1 parent 7c60e76 commit 55cd1ea

File tree

6 files changed

+62
-39
lines changed

6 files changed

+62
-39
lines changed

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ any parts of the framework not mentioned in the documentation should generally b
2626
* Avoid patch on `RelationshipView` deleting relationship instance when constraint would allow null ([#242](https://github.com/django-json-api/django-rest-framework-json-api/issues/242))
2727
* Avoid error with related urls when retrieving relationship which is referenced as `ForeignKey` on parent
2828
* Do not render `write_only` relations
29+
* Render empty relationships as `null` for to-one or empty array (`[]`) for to-many relations instead of skipping them
30+
following [specification](https://jsonapi.org/format/#document-resource-object-linkage).
2931

3032

3133
## [2.6.0] - 2018年09月20日
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 2.1.1 on 2018年12月04日 07:38
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('example', '0005_auto_20180922_1508'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='comment',
16+
name='author',
17+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='example.Author'),
18+
),
19+
]

‎example/models.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class Comment(BaseModel):
109109
Author,
110110
null=True,
111111
blank=True,
112+
related_name='comments',
112113
on_delete=models.CASCADE,
113114
)
114115

‎example/tests/unit/test_renderers.py‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from rest_framework_json_api import serializers, views
44
from rest_framework_json_api.renderers import JSONRenderer
55

6-
from example.models import Comment, Entry
6+
from example.models import Author, Comment, Entry
77

88

99
# serializers
@@ -45,8 +45,8 @@ class ReadOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
4545
serializer_class = DummyTestSerializer
4646

4747

48-
def render_dummy_test_serialized_view(view_class):
49-
serializer = view_class.serializer_class(instance=Entry())
48+
def render_dummy_test_serialized_view(view_class, instance):
49+
serializer = view_class.serializer_class(instance=instance)
5050
renderer = JSONRenderer()
5151
return renderer.render(
5252
serializer.data,
@@ -58,22 +58,22 @@ def test_simple_reverse_relation_included_renderer():
5858
Test renderer when a single reverse fk relation is passed.
5959
'''
6060
rendered = render_dummy_test_serialized_view(
61-
DummyTestViewSet)
61+
DummyTestViewSet, Entry())
6262

6363
assert rendered
6464

6565

6666
def test_simple_reverse_relation_included_read_only_viewset():
6767
rendered = render_dummy_test_serialized_view(
68-
ReadOnlyDummyTestViewSet)
68+
ReadOnlyDummyTestViewSet, Entry())
6969

7070
assert rendered
7171

7272

7373
def test_render_format_field_names(settings):
7474
"""Test that json field is kept untouched."""
7575
settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
76-
rendered = render_dummy_test_serialized_view(DummyTestViewSet)
76+
rendered = render_dummy_test_serialized_view(DummyTestViewSet, Entry())
7777

7878
result = json.loads(rendered.decode())
7979
assert result['data']['attributes']['json-field'] == {'JsonKey': 'JsonValue'}
@@ -83,17 +83,15 @@ def test_render_format_keys(settings):
8383
"""Test that json field value keys are formated."""
8484
delattr(settings, 'JSON_API_FORMAT_FILED_NAMES')
8585
settings.JSON_API_FORMAT_KEYS = 'dasherize'
86-
rendered = render_dummy_test_serialized_view(DummyTestViewSet)
86+
rendered = render_dummy_test_serialized_view(DummyTestViewSet, Entry())
8787

8888
result = json.loads(rendered.decode())
8989
assert result['data']['attributes']['json-field'] == {'json-key': 'JsonValue'}
9090

9191

92-
def test_writeonly_not_in_response(settings):
92+
def test_writeonly_not_in_response():
9393
"""Test that writeonly fields are not shown in list response"""
9494

95-
settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
96-
9795
class WriteonlyTestSerializer(serializers.ModelSerializer):
9896
'''Serializer for testing the absence of write_only fields'''
9997
comments = serializers.ResourceRelatedField(
@@ -112,8 +110,29 @@ class WriteOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
112110
queryset = Entry.objects.all()
113111
serializer_class = WriteonlyTestSerializer
114112

115-
rendered = render_dummy_test_serialized_view(WriteOnlyDummyTestViewSet)
113+
rendered = render_dummy_test_serialized_view(WriteOnlyDummyTestViewSet, Entry())
116114
result = json.loads(rendered.decode())
117115

118116
assert 'rating' not in result['data']['attributes']
119117
assert 'relationships' not in result['data']
118+
119+
120+
def test_render_empty_relationship():
121+
"""Test that empty relationships are rendered as None."""
122+
123+
class EmptyRelationshipSerializer(serializers.ModelSerializer):
124+
class Meta:
125+
model = Author
126+
fields = ('bio', 'comments')
127+
128+
class EmptyRelationshipViewSet(views.ReadOnlyModelViewSet):
129+
queryset = Author.objects.all()
130+
serializer_class = EmptyRelationshipSerializer
131+
132+
rendered = render_dummy_test_serialized_view(EmptyRelationshipViewSet, Author())
133+
result = json.loads(rendered.decode())
134+
assert 'relationships' in result['data']
135+
assert 'bio' in result['data']['relationships']
136+
assert result['data']['relationships']['bio'] == {'data': None}
137+
assert 'comments' in result['data']['relationships']
138+
assert result['data']['relationships']['comments'] == {'meta': {'count': 0}, 'data': []}

‎rest_framework_json_api/renderers.py‎

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,9 @@ def extract_relationships(cls, fields, resource, resource_instance):
113113
relation_type = utils.get_related_resource_type(field)
114114

115115
if isinstance(field, relations.HyperlinkedIdentityField):
116-
resolved, relation_instance = utils.get_relation_instance(
116+
relation_instance = utils.get_relation_instance(
117117
resource_instance, source, field.parent
118118
)
119-
if not resolved:
120-
continue
121119
# special case for HyperlinkedIdentityField
122120
relation_data = list()
123121

@@ -150,13 +148,6 @@ def extract_relationships(cls, fields, resource, resource_instance):
150148
data.update({field_name: relation_data})
151149

152150
if isinstance(field, (ResourceRelatedField, )):
153-
relation_instance_id = getattr(resource_instance, source + "_id", None)
154-
if not relation_instance_id:
155-
resolved, relation_instance = utils.get_relation_instance(resource_instance,
156-
source, field.parent)
157-
if not resolved:
158-
continue
159-
160151
if not isinstance(field, SkipDataMixin):
161152
relation_data.update({'data': resource.get(field_name)})
162153

@@ -166,11 +157,9 @@ def extract_relationships(cls, fields, resource, resource_instance):
166157
if isinstance(
167158
field, (relations.PrimaryKeyRelatedField, relations.HyperlinkedRelatedField)
168159
):
169-
resolved, relation = utils.get_relation_instance(
160+
relation = utils.get_relation_instance(
170161
resource_instance, '%s_id' % source, field.parent
171162
)
172-
if not resolved:
173-
continue
174163
relation_id = relation if resource.get(field_name) else None
175164
relation_data = {
176165
'data': (
@@ -195,11 +184,9 @@ def extract_relationships(cls, fields, resource, resource_instance):
195184
continue
196185

197186
if isinstance(field, relations.ManyRelatedField):
198-
resolved, relation_instance = utils.get_relation_instance(
187+
relation_instance = utils.get_relation_instance(
199188
resource_instance, source, field.parent
200189
)
201-
if not resolved:
202-
continue
203190

204191
relation_data = {}
205192

@@ -230,7 +217,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
230217
continue
231218

232219
relation_data = list()
233-
for nested_resource_instance in relation_instance:
220+
for nested_resource_instance in (relation_instanceor []):
234221
nested_resource_instance_type = (
235222
relation_type or
236223
utils.get_resource_type_from_instance(nested_resource_instance)
@@ -251,16 +238,14 @@ def extract_relationships(cls, fields, resource, resource_instance):
251238
continue
252239

253240
if isinstance(field, ListSerializer):
254-
resolved, relation_instance = utils.get_relation_instance(
241+
relation_instance = utils.get_relation_instance(
255242
resource_instance, source, field.parent
256243
)
257-
if not resolved:
258-
continue
259244

260245
relation_data = list()
261246

262247
serializer_data = resource.get(field_name)
263-
resource_instance_queryset = list(relation_instance)
248+
resource_instance_queryset = list(relation_instanceor [])
264249
if isinstance(serializer_data, list):
265250
for position in range(len(serializer_data)):
266251
nested_resource_instance = resource_instance_queryset[position]
@@ -280,11 +265,9 @@ def extract_relationships(cls, fields, resource, resource_instance):
280265
if isinstance(field, Serializer):
281266
relation_instance_id = getattr(resource_instance, source + "_id", None)
282267
if not relation_instance_id:
283-
resolved, relation_instance = utils.get_relation_instance(
268+
relation_instance = utils.get_relation_instance(
284269
resource_instance, source, field.parent
285270
)
286-
if not resolved:
287-
continue
288271

289272
if relation_instance is not None:
290273
relation_instance_id = relation_instance.pk

‎rest_framework_json_api/utils.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,21 +356,20 @@ def get_included_serializers(serializer):
356356

357357

358358
def get_relation_instance(resource_instance, source, serializer):
359+
relation_instance = None
360+
359361
try:
360362
relation_instance = operator.attrgetter(source)(resource_instance)
361363
except AttributeError:
362364
# if the field is not defined on the model then we check the serializer
363-
# and if no value is there we skip over the field completely
364365
serializer_method = getattr(serializer, source, None)
365366
if serializer_method and hasattr(serializer_method, '__call__'):
366367
relation_instance = serializer_method(resource_instance)
367-
else:
368-
return False, None
369368

370369
if isinstance(relation_instance, Manager):
371370
relation_instance = relation_instance.all()
372371

373-
return True, relation_instance
372+
return relation_instance
374373

375374

376375
class Hyperlink(six.text_type):

0 commit comments

Comments
(0)

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