Skip to content

Overall improvements to parser. #393

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

Closed
wants to merge 1 commit into from
Closed
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
92 changes: 63 additions & 29 deletions rest_framework_json_api/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from rest_framework import parsers
from rest_framework.exceptions import ParseError

from . import exceptions, renderers, serializers, utils
from . import exceptions, renderers, utils
from .serializers import PolymorphicModelSerializer, ResourceIdentifierObjectSerializer


class JSONParser(parsers.JSONParser):
Expand Down Expand Up @@ -74,6 +75,29 @@ def parse_metadata(result):
def parse(self, stream, media_type=None, parser_context=None):
"""
Parses the incoming bytestream as JSON and returns the resulting data

There are two basic object types in JSON-API.

1. Resource Identifier Object

They only have 'id' and 'type' keys (optionally also 'meta'). The 'type'
should be passed to the views for processing. These objects are used in
'relationships' keys and also as the actual 'data' in Relationship URLs.

2. Resource Objects

They use the keys as above plus optional 'attributes' and
'relationships'. Attributes and relationships should be flattened before
sending to views and the 'type' key should be removed.

We support requests with list data. In JSON-API list data can be found
in Relationship URLs where we would expect Resource Identifier Objects,
but we will also allow lists of Resource Objects as the users might want
to implement bulk operations in their custom views.

In addition True, False and None will be accepted as data and passed to
views. In JSON-API None is a valid data for 1-to-1 Relationship URLs and
indicates that the relationship should be cleared.
"""
result = super(JSONParser, self).parse(
stream, media_type=media_type, parser_context=parser_context
Expand All @@ -84,32 +108,39 @@ def parse(self, stream, media_type=None, parser_context=None):

data = result.get('data')
view = parser_context['view']

from rest_framework_json_api.views import RelationshipView
if isinstance(view, RelationshipView):
# We skip parsing the object as JSONAPI Resource Identifier Object and not a regular
# Resource Object
if isinstance(data, list):
for resource_identifier_object in data:
if not (
resource_identifier_object.get('id') and
resource_identifier_object.get('type')
):
raise ParseError(
'Received data contains one or more malformed JSONAPI '
'Resource Identifier Object(s)'
)
elif not (data.get('id') and data.get('type')):
raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object')

resource_name = utils.get_resource_name(parser_context, expand_polymorphic_types=True)
method = parser_context.get('request').method
serializer_class = getattr(view, 'serializer_class', None)
in_relationship_view = serializer_class == ResourceIdentifierObjectSerializer

if isinstance(data, list):
for item in data:
if not isinstance(item, dict):
err = "Items in data array must be objects with 'id' and 'type' members."
raise ParseError(err)

if in_relationship_view:
for identifier in data:
self.verify_resource_identifier(identifier)
return data
else:
return list(
self.parse_resource(d, d, resource_name, method, serializer_class)
for d in data
)
elif isinstance(data, dict):
if in_relationship_view:
self.verify_resource_identifier(data)
return data
else:
return self.parse_resource(data, result, resource_name, method, serializer_class)
else:
# None, True, False, numbers and strings
return data

request = parser_context.get('request')

def parse_resource(self, data, meta_source, resource_name, method, serializer_class):
# Check for inconsistencies
if request.method in ('PUT', 'POST', 'PATCH'):
resource_name = utils.get_resource_name(
parser_context, expand_polymorphic_types=True)
if method in ('PUT', 'POST', 'PATCH'):
if isinstance(resource_name, six.string_types):
if data.get('type') != resource_name:
raise exceptions.Conflict(
Expand All @@ -126,17 +157,20 @@ def parse(self, stream, media_type=None, parser_context=None):
"(one of [{resource_types}]).".format(
data_type=data.get('type'),
resource_types=", ".join(resource_name)))
if not data.get('id') and request.method in ('PATCH', 'PUT'):
raise ParseError("The resource identifier object must contain an 'id' member")
if not data.get('id') and method in ('PATCH', 'PUT'):
raise ParseError("The resource object must contain an 'id' member.")

# Construct the return data
serializer_class = getattr(view, 'serializer_class', None)
parsed_data = {'id': data.get('id')} if 'id' in data else {}
# `type` field needs to be allowed in none polymorphic serializers
if serializer_class is not None:
if issubclass(serializer_class, serializers.PolymorphicModelSerializer):
if issubclass(serializer_class, PolymorphicModelSerializer):
parsed_data['type'] = data.get('type')
parsed_data.update(self.parse_attributes(data))
parsed_data.update(self.parse_relationships(data))
parsed_data.update(self.parse_metadata(result))
parsed_data.update(self.parse_metadata(meta_source))
return parsed_data

def verify_resource_identifier(self, data):
if not data.get('id') or not data.get('type'):
raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object(s).')