Skip to content

Commit 4ed4e4e

Browse files
committed
Overall improvements to parser.
- Less exceptions caused by data type assumptions - Better handling of list data - Does not special-case RelationshipView
1 parent b0257c0 commit 4ed4e4e

File tree

1 file changed

+63
-29
lines changed

1 file changed

+63
-29
lines changed

rest_framework_json_api/parsers.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from rest_framework import parsers
77
from rest_framework.exceptions import ParseError
88

9-
from . import exceptions, renderers, serializers, utils
9+
from . import exceptions, renderers, utils
10+
from .serializers import PolymorphicModelSerializer, ResourceIdentifierObjectSerializer
1011

1112

1213
class JSONParser(parsers.JSONParser):
@@ -74,6 +75,29 @@ def parse_metadata(result):
7475
def parse(self, stream, media_type=None, parser_context=None):
7576
"""
7677
Parses the incoming bytestream as JSON and returns the resulting data
78+
79+
There are two basic object types in JSON-API.
80+
81+
1. Resource Identifier Object
82+
83+
They only have 'id' and 'type' keys (optionally also 'meta'). The 'type'
84+
should be passed to the views for processing. These objects are used in
85+
'relationships' keys and also as the actual 'data' in Relationship URLs.
86+
87+
2. Resource Objects
88+
89+
They use the keys as above plus optional 'attributes' and
90+
'relationships'. Attributes and relationships should be flattened before
91+
sending to views and the 'type' key should be removed.
92+
93+
We support requests with list data. In JSON-API list data can be found
94+
in Relationship URLs where we would expect Resource Identifier Objects,
95+
but we will also allow lists of Resource Objects as the users might want
96+
to implement bulk operations in their custom views.
97+
98+
In addition True, False and None will be accepted as data and passed to
99+
views. In JSON-API None is a valid data for 1-to-1 Relationship URLs and
100+
indicates that the relationship should be cleared.
77101
"""
78102
result = super(JSONParser, self).parse(
79103
stream, media_type=media_type, parser_context=parser_context
@@ -84,32 +108,39 @@ def parse(self, stream, media_type=None, parser_context=None):
84108

85109
data = result.get('data')
86110
view = parser_context['view']
87-
88-
from rest_framework_json_api.views import RelationshipView
89-
if isinstance(view, RelationshipView):
90-
# We skip parsing the object as JSONAPI Resource Identifier Object and not a regular
91-
# Resource Object
92-
if isinstance(data, list):
93-
for resource_identifier_object in data:
94-
if not (
95-
resource_identifier_object.get('id') and
96-
resource_identifier_object.get('type')
97-
):
98-
raise ParseError(
99-
'Received data contains one or more malformed JSONAPI '
100-
'Resource Identifier Object(s)'
101-
)
102-
elif not (data.get('id') and data.get('type')):
103-
raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object')
104-
111+
resource_name = utils.get_resource_name(parser_context, expand_polymorphic_types=True)
112+
method = parser_context.get('request').method
113+
serializer_class = getattr(view, 'serializer_class', None)
114+
in_relationship_view = serializer_class == ResourceIdentifierObjectSerializer
115+
116+
if isinstance(data, list):
117+
for item in data:
118+
if not isinstance(item, dict):
119+
err = "Items in data array must be objects with 'id' and 'type' members."
120+
raise ParseError(err)
121+
122+
if in_relationship_view:
123+
for identifier in data:
124+
self.verify_resource_identifier(identifier)
125+
return data
126+
else:
127+
return list(
128+
self.parse_resource(d, d, resource_name, method, serializer_class)
129+
for d in data
130+
)
131+
elif isinstance(data, dict):
132+
if in_relationship_view:
133+
self.verify_resource_identifier(data)
134+
return data
135+
else:
136+
return self.parse_resource(data, result, resource_name, method, serializer_class)
137+
else:
138+
# None, True, False, numbers and strings
105139
return data
106140

107-
request = parser_context.get('request')
108-
141+
def parse_resource(self, data, meta_source, resource_name, method, serializer_class):
109142
# Check for inconsistencies
110-
if request.method in ('PUT', 'POST', 'PATCH'):
111-
resource_name = utils.get_resource_name(
112-
parser_context, expand_polymorphic_types=True)
143+
if method in ('PUT', 'POST', 'PATCH'):
113144
if isinstance(resource_name, six.string_types):
114145
if data.get('type') != resource_name:
115146
raise exceptions.Conflict(
@@ -126,17 +157,20 @@ def parse(self, stream, media_type=None, parser_context=None):
126157
"(one of [{resource_types}]).".format(
127158
data_type=data.get('type'),
128159
resource_types=", ".join(resource_name)))
129-
if not data.get('id') and request.method in ('PATCH', 'PUT'):
130-
raise ParseError("The resource identifier object must contain an 'id' member")
160+
if not data.get('id') and method in ('PATCH', 'PUT'):
161+
raise ParseError("The resource object must contain an 'id' member.")
131162

132163
# Construct the return data
133-
serializer_class = getattr(view, 'serializer_class', None)
134164
parsed_data = {'id': data.get('id')} if 'id' in data else {}
135165
# `type` field needs to be allowed in none polymorphic serializers
136166
if serializer_class is not None:
137-
if issubclass(serializer_class, serializers.PolymorphicModelSerializer):
167+
if issubclass(serializer_class, PolymorphicModelSerializer):
138168
parsed_data['type'] = data.get('type')
139169
parsed_data.update(self.parse_attributes(data))
140170
parsed_data.update(self.parse_relationships(data))
141-
parsed_data.update(self.parse_metadata(result))
171+
parsed_data.update(self.parse_metadata(meta_source))
142172
return parsed_data
173+
174+
def verify_resource_identifier(self, data):
175+
if not data.get('id') or not data.get('type'):
176+
raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object(s).')

0 commit comments

Comments
 (0)