diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 86a61e74..a9091d0e 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -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): @@ -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 @@ -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( @@ -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).')