Serializer API restrictions.

This commit is contained in:
Tom Christie 2014-12-17 14:14:51 +00:00
parent 426547c61c
commit c6137bbf5a
5 changed files with 62 additions and 21 deletions

View File

@ -240,6 +240,12 @@ Serializer classes can also include reusable validators that are applied to the
For more information see the [validators documentation](validators.md). For more information see the [validators documentation](validators.md).
## Accessing the initial data and instance
When passing an initial object or queryset to a serializer instance, the object will be made available as `.instance`. If no initial object is passed then the `.instance` attribute will be `None`.
When passing data to a serializer instance, the unmodified data will be made available as `.initial_data`. If the data keyword argument is not passed then the `.initial_data` attribute will not exist.
## Partial updates ## Partial updates
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates. By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates.

View File

@ -79,16 +79,14 @@ class GenericAPIView(views.APIView):
'view': self 'view': self
} }
def get_serializer(self, instance=None, data=None, many=False, partial=False): def get_serializer(self, *args, **kwargs):
""" """
Return the serializer instance that should be used for validating and Return the serializer instance that should be used for validating and
deserializing input, and for serializing output. deserializing input, and for serializing output.
""" """
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
context = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return serializer_class( return serializer_class(*args, **kwargs)
instance, data=data, many=many, partial=partial, context=context
)
def get_pagination_serializer(self, page): def get_pagination_serializer(self, page):
""" """

View File

@ -544,12 +544,12 @@ class BrowsableAPIRenderer(BaseRenderer):
# serializer instance, rather than dynamically creating a new one. # serializer instance, rather than dynamically creating a new one.
if request.method == method and serializer is not None: if request.method == method and serializer is not None:
try: try:
data = request.data kwargs = {'data': request.data}
except ParseError: except ParseError:
data = None kwargs = {}
existing_serializer = serializer existing_serializer = serializer
else: else:
data = None kwargs = {}
existing_serializer = None existing_serializer = None
with override_method(view, request, method) as request: with override_method(view, request, method) as request:
@ -569,11 +569,13 @@ class BrowsableAPIRenderer(BaseRenderer):
serializer = existing_serializer serializer = existing_serializer
else: else:
if method in ('PUT', 'PATCH'): if method in ('PUT', 'PATCH'):
serializer = view.get_serializer(instance=instance, data=data) serializer = view.get_serializer(instance=instance, **kwargs)
else: else:
serializer = view.get_serializer(data=data) serializer = view.get_serializer(**kwargs)
if data is not None:
if hasattr(serializer, 'initial_data'):
serializer.is_valid() serializer.is_valid()
form_renderer = self.form_renderer_class() form_renderer = self.form_renderer_class()
return form_renderer.render( return form_renderer.render(
serializer.data, serializer.data,

View File

@ -58,11 +58,31 @@ class BaseSerializer(Field):
""" """
The BaseSerializer class provides a minimal class which may be used The BaseSerializer class provides a minimal class which may be used
for writing custom serializer implementations. for writing custom serializer implementations.
Note that we strongly restrict the ordering of operations/properties
that may be used on the serializer in order to enforce correct usage.
In particular, if a `data=` argument is passed then:
.is_valid() - Available.
.initial_data - Available.
.validated_data - Only available after calling `is_valid()`
.errors - Only available after calling `is_valid()`
.data - Only available after calling `is_valid()`
If a `data=` argument is not passed then:
.is_valid() - Not available.
.initial_data - Not available.
.validated_data - Not available.
.errors - Not available.
.data - Available.
""" """
def __init__(self, instance=None, data=None, **kwargs): def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance self.instance = instance
self._initial_data = data if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False) self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {}) self._context = kwargs.pop('context', {})
kwargs.pop('many', None) kwargs.pop('many', None)
@ -156,9 +176,14 @@ class BaseSerializer(Field):
(self.__class__.__module__, self.__class__.__name__) (self.__class__.__module__, self.__class__.__name__)
) )
assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was'
'passed when instantiating the serializer instance.'
)
if not hasattr(self, '_validated_data'): if not hasattr(self, '_validated_data'):
try: try:
self._validated_data = self.run_validation(self._initial_data) self._validated_data = self.run_validation(self.initial_data)
except ValidationError as exc: except ValidationError as exc:
self._validated_data = {} self._validated_data = {}
self._errors = exc.detail self._errors = exc.detail
@ -172,6 +197,16 @@ class BaseSerializer(Field):
@property @property
def data(self): def data(self):
if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
msg = (
'When a serializer is passed a `data` keyword argument you '
'must call `.is_valid()` before attempting to access the '
'serialized `.data` representation.\n'
'You should either call `.is_valid()` first, '
'or access `.initial_data` instead.'
)
raise AssertionError(msg)
if not hasattr(self, '_data'): if not hasattr(self, '_data'):
if self.instance is not None and not getattr(self, '_errors', None): if self.instance is not None and not getattr(self, '_errors', None):
self._data = self.to_representation(self.instance) self._data = self.to_representation(self.instance)
@ -295,11 +330,11 @@ class Serializer(BaseSerializer):
return getattr(getattr(self, 'Meta', None), 'validators', []) return getattr(getattr(self, 'Meta', None), 'validators', [])
def get_initial(self): def get_initial(self):
if self._initial_data is not None: if hasattr(self, 'initial_data'):
return OrderedDict([ return OrderedDict([
(field_name, field.get_value(self._initial_data)) (field_name, field.get_value(self.initial_data))
for field_name, field in self.fields.items() for field_name, field in self.fields.items()
if field.get_value(self._initial_data) is not empty if field.get_value(self.initial_data) is not empty
and not field.read_only and not field.read_only
]) ])
@ -447,8 +482,8 @@ class ListSerializer(BaseSerializer):
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
def get_initial(self): def get_initial(self):
if self._initial_data is not None: if hasattr(self, 'initial_data'):
return self.to_representation(self._initial_data) return self.to_representation(self.initial_data)
return [] return []
def get_value(self, dictionary): def get_value(self, dictionary):

View File

@ -22,7 +22,7 @@ class TestSimpleBoundField:
amount = serializers.IntegerField() amount = serializers.IntegerField()
serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123}) serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
assert serializer.is_valid()
assert serializer['text'].value == 'abc' assert serializer['text'].value == 'abc'
assert serializer['text'].errors is None assert serializer['text'].errors is None
assert serializer['text'].name == 'text' assert serializer['text'].name == 'text'