aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/serializers.py
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework/serializers.py')
-rw-r--r--rest_framework/serializers.py1896
1 files changed, 1160 insertions, 736 deletions
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 682a99a4..2eef6eeb 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -6,20 +6,31 @@ form encoded input.
Serialization in REST framework is a two-phase process:
1. Serializers marshal between complex types like model instances, and
-python primatives.
-2. The process of marshalling between python primatives and request and
+python primitives.
+2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
from __future__ import unicode_literals
-import copy
-import datetime
-import types
-from decimal import Decimal
-from django.core.paginator import Page
from django.db import models
-from django.forms import widgets
-from django.utils.datastructures import SortedDict
-from rest_framework.compat import get_concrete_model, six
+from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
+from django.db.models import query
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.compat import postgres_fields, unicode_to_repr
+from rest_framework.utils import model_meta
+from rest_framework.utils.field_mapping import (
+ get_url_kwargs, get_field_kwargs,
+ get_relation_kwargs, get_nested_relation_kwargs,
+ ClassLookupDict
+)
+from rest_framework.utils.serializer_helpers import (
+ ReturnDict, ReturnList, BoundField, NestedBoundField, BindingDict
+)
+from rest_framework.validators import (
+ UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
+ UniqueTogetherValidator
+)
+import warnings
+
# Note: We do the following so that users of the framework can use this style:
#
@@ -28,941 +39,1354 @@ from rest_framework.compat import get_concrete_model, six
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
-from rest_framework.relations import *
-from rest_framework.fields import *
+from rest_framework.relations import * # NOQA
+from rest_framework.fields import * # NOQA
-class NestedValidationError(ValidationError):
- """
- The default ValidationError behavior is to stringify each item in the list
- if the messages are a list of error messages.
+# We assume that 'validators' are intended for the child serializer,
+# rather than the parent serializer.
+LIST_SERIALIZER_KWARGS = (
+ 'read_only', 'write_only', 'required', 'default', 'initial', 'source',
+ 'label', 'help_text', 'style', 'error_messages',
+ 'instance', 'data', 'partial', 'context'
+)
+
- In the case of nested serializers, where the parent has many children,
- then the child's `serializer.errors` will be a list of dicts. In the case
- of a single child, the `serializer.errors` will be a dict.
+# BaseSerializer
+# --------------
- We need to override the default behavior to get properly nested error dicts.
+class BaseSerializer(Field):
"""
+ The BaseSerializer class provides a minimal class which may be used
+ for writing custom serializer implementations.
- def __init__(self, message):
- if isinstance(message, dict):
- self.messages = [message]
- else:
- self.messages = message
+ 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:
-class DictWithMetadata(dict):
- """
- A dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
- """
- Used by pickle (e.g., caching).
- Overridden to remove the metadata from the dict, since it shouldn't be
- pickled and may in some instances be unpickleable.
- """
- return dict(self)
+ .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:
-class SortedDictWithMetadata(SortedDict):
+ .is_valid() - Not available.
+ .initial_data - Not available.
+ .validated_data - Not available.
+ .errors - Not available.
+ .data - Available.
"""
- A sorted dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
+
+ def __init__(self, instance=None, data=empty, **kwargs):
+ self.instance = instance
+ if data is not empty:
+ self.initial_data = data
+ self.partial = kwargs.pop('partial', False)
+ self._context = kwargs.pop('context', {})
+ kwargs.pop('many', None)
+ super(BaseSerializer, self).__init__(**kwargs)
+
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ListSerializer` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ return cls.many_init(*args, **kwargs)
+ return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
"""
- Used by pickle (e.g., caching).
- Overriden to remove the metadata from the dict, since it shouldn't be
- pickle and may in some instances be unpickleable.
+ This method implements the creation of a `ListSerializer` parent
+ class when `many=True` is used. You can customize it if you need to
+ control which keyword arguments are passed to the parent, and
+ which are passed to the child.
+
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomListSerializer(*args, **kwargs)
"""
- return SortedDict(self).__dict__
+ child_serializer = cls(*args, **kwargs)
+ list_kwargs = {'child': child_serializer}
+ list_kwargs.update(dict([
+ (key, value) for key, value in kwargs.items()
+ if key in LIST_SERIALIZER_KWARGS
+ ]))
+ meta = getattr(cls, 'Meta', None)
+ list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
+ return list_serializer_class(*args, **list_kwargs)
+ def to_internal_value(self, data):
+ raise NotImplementedError('`to_internal_value()` must be implemented.')
-def _is_protected_type(obj):
- """
- True if the object is a native datatype that does not need to
- be serialized further.
- """
- return isinstance(obj, (
- types.NoneType,
- int, long,
- datetime.datetime, datetime.date, datetime.time,
- float, Decimal,
- basestring)
- )
+ def to_representation(self, instance):
+ raise NotImplementedError('`to_representation()` must be implemented.')
+ def update(self, instance, validated_data):
+ raise NotImplementedError('`update()` must be implemented.')
-def _get_declared_fields(bases, attrs):
- """
- Create a list of serializer field instances from the passed in 'attrs',
- plus any fields on the base classes (in 'bases').
+ def create(self, validated_data):
+ raise NotImplementedError('`create()` must be implemented.')
- Note that all fields from the base classes are used.
- """
- fields = [(field_name, attrs.pop(field_name))
- for field_name, obj in list(six.iteritems(attrs))
- if isinstance(obj, Field)]
- fields.sort(key=lambda x: x[1].creation_counter)
+ def save(self, **kwargs):
+ assert not hasattr(self, 'save_object'), (
+ 'Serializer `%s.%s` has old-style version 2 `.save_object()` '
+ 'that is no longer compatible with REST framework 3. '
+ 'Use the new-style `.create()` and `.update()` methods instead.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
- # If this class is subclassing another Serializer, add that Serializer's
- # fields. Note that we loop over the bases in *reverse*. This is necessary
- # in order to maintain the correct order of fields.
- for base in bases[::-1]:
- if hasattr(base, 'base_fields'):
- fields = list(base.base_fields.items()) + fields
+ assert hasattr(self, '_errors'), (
+ 'You must call `.is_valid()` before calling `.save()`.'
+ )
- return SortedDict(fields)
+ assert not self.errors, (
+ 'You cannot call `.save()` on a serializer with invalid data.'
+ )
+ validated_data = dict(
+ list(self.validated_data.items()) +
+ list(kwargs.items())
+ )
-class SerializerMetaclass(type):
- def __new__(cls, name, bases, attrs):
- attrs['base_fields'] = _get_declared_fields(bases, attrs)
- return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+ if self.instance is not None:
+ self.instance = self.update(self.instance, validated_data)
+ assert self.instance is not None, (
+ '`update()` did not return an object instance.'
+ )
+ else:
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
+ return self.instance
-class SerializerOptions(object):
- """
- Meta class options for Serializer
- """
- def __init__(self, meta):
- self.depth = getattr(meta, 'depth', 0)
- self.fields = getattr(meta, 'fields', ())
- self.exclude = getattr(meta, 'exclude', ())
+ def is_valid(self, raise_exception=False):
+ assert not hasattr(self, 'restore_object'), (
+ 'Serializer `%s.%s` has old-style version 2 `.restore_object()` '
+ 'that is no longer compatible with REST framework 3. '
+ 'Use the new-style `.create()` and `.update()` methods instead.' %
+ (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.'
+ )
-class BaseSerializer(WritableField):
- """
- This is the Serializer implementation.
- We need to implement it as `BaseSerializer` due to metaclass magicks.
- """
- class Meta(object):
- pass
+ if not hasattr(self, '_validated_data'):
+ try:
+ self._validated_data = self.run_validation(self.initial_data)
+ except ValidationError as exc:
+ self._validated_data = {}
+ self._errors = exc.detail
+ else:
+ self._errors = {}
- _options_class = SerializerOptions
- _dict_class = SortedDictWithMetadata
+ if self._errors and raise_exception:
+ raise ValidationError(self._errors)
- def __init__(self, instance=None, data=None, files=None,
- context=None, partial=False, many=None,
- allow_add_remove=False, **kwargs):
- super(BaseSerializer, self).__init__(**kwargs)
- self.opts = self._options_class(self.Meta)
- self.parent = None
- self.root = None
- self.partial = partial
- self.many = many
- self.allow_add_remove = allow_add_remove
+ return not bool(self._errors)
- self.context = context or {}
+ @property
+ 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)
- self.init_data = data
- self.init_files = files
- self.object = instance
- self.fields = self.get_fields()
+ if not hasattr(self, '_data'):
+ if self.instance is not None and not getattr(self, '_errors', None):
+ self._data = self.to_representation(self.instance)
+ elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
+ self._data = self.to_representation(self.validated_data)
+ else:
+ self._data = self.get_initial()
+ return self._data
- self._data = None
- self._files = None
- self._errors = None
- self._deleted = None
+ @property
+ def errors(self):
+ if not hasattr(self, '_errors'):
+ msg = 'You must call `.is_valid()` before accessing `.errors`.'
+ raise AssertionError(msg)
+ return self._errors
- if many and instance is not None and not hasattr(instance, '__iter__'):
- raise ValueError('instance should be a queryset or other iterable with many=True')
+ @property
+ def validated_data(self):
+ if not hasattr(self, '_validated_data'):
+ msg = 'You must call `.is_valid()` before accessing `.validated_data`.'
+ raise AssertionError(msg)
+ return self._validated_data
- if allow_add_remove and not many:
- raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
- #####
- # Methods to determine which fields to use when (de)serializing objects.
+# Serializer & ListSerializer classes
+# -----------------------------------
- def get_default_fields(self):
- """
- Return the complete set of default fields for the object, as a dict.
- """
- return {}
+class SerializerMetaclass(type):
+ """
+ This metaclass sets a dictionary named `_declared_fields` on the class.
- def get_fields(self):
- """
- Returns the complete set of fields for the object as a dict.
+ Any instances of `Field` included as attributes on either the class
+ or on any of its superclasses will be include in the
+ `_declared_fields` dictionary.
+ """
- This will be the set of any explicitly declared fields,
- plus the set of fields returned by get_default_fields().
- """
- ret = SortedDict()
+ @classmethod
+ def _get_declared_fields(cls, bases, attrs):
+ fields = [(field_name, attrs.pop(field_name))
+ for field_name, obj in list(attrs.items())
+ if isinstance(obj, Field)]
+ fields.sort(key=lambda x: x[1]._creation_counter)
- # Get the explicitly declared fields
- base_fields = copy.deepcopy(self.base_fields)
- for key, field in base_fields.items():
- ret[key] = field
+ # If this class is subclassing another Serializer, add that Serializer's
+ # fields. Note that we loop over the bases in *reverse*. This is necessary
+ # in order to maintain the correct order of fields.
+ for base in reversed(bases):
+ if hasattr(base, '_declared_fields'):
+ fields = list(base._declared_fields.items()) + fields
- # Add in the default fields
- default_fields = self.get_default_fields()
- for key, val in default_fields.items():
- if key not in ret:
- ret[key] = val
+ return OrderedDict(fields)
- # If 'fields' is specified, use those fields, in that order.
- if self.opts.fields:
- assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
- new = SortedDict()
- for key in self.opts.fields:
- new[key] = ret[key]
- ret = new
+ def __new__(cls, name, bases, attrs):
+ attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
+ return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
- # Remove anything in 'exclude'
- if self.opts.exclude:
- assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
- for key in self.opts.exclude:
- ret.pop(key, None)
- for key, field in ret.items():
- field.initialize(parent=self, field_name=key)
+def get_validation_error_detail(exc):
+ assert isinstance(exc, (ValidationError, DjangoValidationError))
- return ret
+ if isinstance(exc, DjangoValidationError):
+ # Normally you should raise `serializers.ValidationError`
+ # inside your codebase, but we handle Django's validation
+ # exception class as well for simpler compat.
+ # Eg. Calling Model.clean() explicitly inside Serializer.validate()
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
+ }
+ elif isinstance(exc.detail, dict):
+ # If errors may be a dict we use the standard {key: list of values}.
+ # Here we ensure that all the values are *lists* of errors.
+ return dict([
+ (key, value if isinstance(value, list) else [value])
+ for key, value in exc.detail.items()
+ ])
+ elif isinstance(exc.detail, list):
+ # Errors raised as a list are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+ }
+ # Errors raised as a string are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+ }
- #####
- # Methods to convert or revert from objects <--> primitive representations.
- def get_field_key(self, field_name):
+@six.add_metaclass(SerializerMetaclass)
+class Serializer(BaseSerializer):
+ default_error_messages = {
+ 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
+ }
+
+ @property
+ def fields(self):
"""
- Return the key that should be used for a given field.
+ A dictionary of {field_name: field_instance}.
"""
- return field_name
+ # `fields` is evaluated lazily. We do this to ensure that we don't
+ # have issues importing modules that use ModelSerializers as fields,
+ # even if Django's app-loading stage has not yet run.
+ if not hasattr(self, '_fields'):
+ self._fields = BindingDict(self)
+ for key, value in self.get_fields().items():
+ self._fields[key] = value
+ return self._fields
- def restore_fields(self, data, files):
+ def get_fields(self):
"""
- Core of deserialization, together with `restore_object`.
- Converts a dictionary of data into a dictionary of deserialized fields.
+ Returns a dictionary of {field_name: field_instance}.
"""
- reverted_data = {}
+ # Every new serializer is created with a clone of the field instances.
+ # This allows users to dynamically modify the fields on a serializer
+ # instance without affecting every other serializer class.
+ return copy.deepcopy(self._declared_fields)
- if data is not None and not isinstance(data, dict):
- self._errors['non_field_errors'] = ['Invalid data']
- return None
+ def get_validators(self):
+ """
+ Returns a list of validator callables.
+ """
+ # Used by the lazily-evaluated `validators` property.
+ meta = getattr(self, 'Meta', None)
+ validators = getattr(meta, 'validators', None)
+ return validators[:] if validators else []
+
+ def get_initial(self):
+ if hasattr(self, 'initial_data'):
+ return OrderedDict([
+ (field_name, field.get_value(self.initial_data))
+ for field_name, field in self.fields.items()
+ if (field.get_value(self.initial_data) is not empty) and
+ not field.read_only
+ ])
+
+ return OrderedDict([
+ (field.field_name, field.get_initial())
+ for field in self.fields.values()
+ if not field.read_only
+ ])
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # nested HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_dict(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- try:
- field.field_from_native(data, files, field_name, reverted_data)
- except ValidationError as err:
- self._errors[field_name] = list(err.messages)
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
- return reverted_data
+ return value
- def perform_validation(self, attrs):
+ def to_internal_value(self, data):
"""
- Run `validate_<fieldname>()` and `validate()` methods on the serializer
+ Dict of native values <- Dict of primitive datatypes.
"""
- for field_name, field in self.fields.items():
- if field_name in self._errors:
- continue
- try:
- validate_method = getattr(self, 'validate_%s' % field_name, None)
- if validate_method:
- source = field.source or field_name
- attrs = validate_method(attrs, source)
- except ValidationError as err:
- self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
-
- # If there are already errors, we don't run .validate() because
- # field-validation failed and thus `attrs` may not be complete.
- # which in turn can cause inconsistent validation errors.
- if not self._errors:
+ if not isinstance(data, dict):
+ message = self.error_messages['invalid'].format(
+ datatype=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
+
+ ret = OrderedDict()
+ errors = OrderedDict()
+ fields = [
+ field for field in self.fields.values()
+ if (not field.read_only) or (field.default is not empty)
+ ]
+
+ for field in fields:
+ validate_method = getattr(self, 'validate_' + field.field_name, None)
+ primitive_value = field.get_value(data)
try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- if hasattr(err, 'message_dict'):
- for field_name, error_messages in err.message_dict.items():
- self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
- elif hasattr(err, 'messages'):
- self._errors['non_field_errors'] = err.messages
+ validated_value = field.run_validation(primitive_value)
+ if validate_method is not None:
+ validated_value = validate_method(validated_value)
+ except ValidationError as exc:
+ errors[field.field_name] = exc.detail
+ except DjangoValidationError as exc:
+ errors[field.field_name] = list(exc.messages)
+ except SkipField:
+ pass
+ else:
+ set_value(ret, field.source_attrs, validated_value)
- return attrs
+ if errors:
+ raise ValidationError(errors)
- def validate(self, attrs):
- """
- Stub method, to be overridden in Serializer subclasses
- """
- return attrs
+ return ret
- def restore_object(self, attrs, instance=None):
+ def to_representation(self, instance):
"""
- Deserialize a dictionary of attributes into an object instance.
- You should override this method to control how deserialized objects
- are instantiated.
+ Object instance -> Dict of primitive datatypes.
"""
- if instance is not None:
- instance.update(attrs)
- return instance
- return attrs
+ ret = OrderedDict()
+ fields = [field for field in self.fields.values() if not field.write_only]
- def to_native(self, obj):
- """
- Serialize objects -> primitives.
- """
- ret = self._dict_class()
- ret.fields = {}
+ for field in fields:
+ try:
+ attribute = field.get_attribute(instance)
+ except SkipField:
+ continue
+
+ if attribute is None:
+ # We skip `to_representation` for `None` values so that
+ # fields do not have to explicitly deal with that case.
+ ret[field.field_name] = None
+ else:
+ ret[field.field_name] = field.to_representation(attribute)
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- key = self.get_field_key(field_name)
- value = field.field_to_native(obj, field_name)
- ret[key] = value
- ret.fields[key] = field
return ret
- def from_native(self, data, files):
- """
- Deserialize primitives -> objects.
- """
- self._errors = {}
- if data is not None or files is not None:
- attrs = self.restore_fields(data, files)
- if attrs is not None:
- attrs = self.perform_validation(attrs)
- else:
- self._errors['non_field_errors'] = ['No input provided']
+ def validate(self, attrs):
+ return attrs
- if not self._errors:
- return self.restore_object(attrs, instance=getattr(self, 'object', None))
+ def __repr__(self):
+ return unicode_to_repr(representation.serializer_repr(self, indent=1))
- def field_to_native(self, obj, field_name):
- """
- Override default so that the serializer can be used as a nested field
- across relationships.
- """
- if self.source == '*':
- return self.to_native(obj)
+ # The following are used for accessing `BoundField` instances on the
+ # serializer, for the purposes of presenting a form-like API onto the
+ # field values and field errors.
- try:
- source = self.source or field_name
- value = obj
+ def __iter__(self):
+ for field in self.fields.values():
+ yield self[field.field_name]
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
- except ObjectDoesNotExist:
- return None
+ def __getitem__(self, key):
+ field = self.fields[key]
+ value = self.data.get(key)
+ error = self.errors.get(key) if hasattr(self, '_errors') else None
+ if isinstance(field, Serializer):
+ return NestedBoundField(field, value, error)
+ return BoundField(field, value, error)
- if is_simple_callable(getattr(value, 'all', None)):
- return [self.to_native(item) for item in value.all()]
+ # Include a backlink to the serializer class on return objects.
+ # Allows renderers such as HTMLFormRenderer to get the full field info.
- if value is None:
- return None
+ @property
+ def data(self):
+ ret = super(Serializer, self).data
+ return ReturnDict(ret, serializer=self)
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type))
+ @property
+ def errors(self):
+ ret = super(Serializer, self).errors
+ return ReturnDict(ret, serializer=self)
- if many:
- return [self.to_native(item) for item in value]
- return self.to_native(value)
- def field_from_native(self, data, files, field_name, into):
- """
- Override default so that the serializer can be used as a writable
- nested field across relationships.
- """
- if self.read_only:
- return
+# There's some replication of `ListField` here,
+# but that's probably better than obfuscating the call hierarchy.
- try:
- value = data[field_name]
- except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- value = copy.deepcopy(self.default)
- else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+class ListSerializer(BaseSerializer):
+ child = None
+ many = True
- # Set the serializer object if it exists
- obj = getattr(self.parent.object, field_name) if self.parent.object else None
+ default_error_messages = {
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
+ }
- if self.source == '*':
- if value:
- into.update(value)
- else:
- if value in (None, ''):
- into[(self.source or field_name)] = None
- else:
- kwargs = {
- 'instance': obj,
- 'data': value,
- 'context': self.context,
- 'partial': self.partial,
- 'many': self.many
- }
- serializer = self.__class__(**kwargs)
-
- if serializer.is_valid():
- into[self.source or field_name] = serializer.object
- else:
- # Propagate errors up to our parent
- raise NestedValidationError(serializer.errors)
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert self.child is not None, '`child` is a required argument.'
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(ListSerializer, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_initial(self):
+ if hasattr(self, 'initial_data'):
+ return self.to_representation(self.initial_data)
+ return []
- def get_identity(self, data):
+ def get_value(self, dictionary):
"""
- This hook is required for bulk update.
- It is used to determine the canonical identity of a given object.
+ Given the input dictionary, return the field value.
+ """
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
- Note that the data has not been validated at this point, so we need
- to make sure that we catch any cases of incorrect datatypes being
- passed to this method.
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
"""
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+
+ value = self.to_internal_value(data)
try:
- return data.get('id', None)
- except AttributeError:
- return None
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
- @property
- def errors(self):
+ return value
+
+ def to_internal_value(self, data):
"""
- Run deserialization and return error data,
- setting self.object if no errors occurred.
+ List of dicts of native values <- List of dicts of primitive datatypes.
"""
- if self._errors is None:
- data, files = self.init_data, self.init_files
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
- if many:
- warnings.warn('Implict list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=3)
-
- if many:
- ret = []
- errors = []
- update = self.object is not None
-
- if update:
- # If this is a bulk update we need to map all the objects
- # to a canonical identity so we can determine which
- # individual object is being updated for each item in the
- # incoming data
- objects = self.object
- identities = [self.get_identity(self.to_native(obj)) for obj in objects]
- identity_to_objects = dict(zip(identities, objects))
-
- if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)):
- for item in data:
- if update:
- # Determine which object we're updating
- identity = self.get_identity(item)
- self.object = identity_to_objects.pop(identity, None)
- if self.object is None and not self.allow_add_remove:
- ret.append(None)
- errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
- continue
-
- ret.append(self.from_native(item, None))
- errors.append(self._errors)
-
- if update:
- self._deleted = identity_to_objects.values()
-
- self._errors = any(errors) and errors or []
- else:
- self._errors = {'non_field_errors': ['Expected a list of items.']}
- else:
- ret = self.from_native(data, files)
+ if not isinstance(data, list):
+ message = self.error_messages['not_a_list'].format(
+ input_type=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
- if not self._errors:
- self.object = ret
+ ret = []
+ errors = []
- return self._errors
+ for item in data:
+ try:
+ validated = self.child.run_validation(item)
+ except ValidationError as exc:
+ errors.append(exc.detail)
+ else:
+ ret.append(validated)
+ errors.append({})
- def is_valid(self):
- return not self.errors
+ if any(errors):
+ raise ValidationError(errors)
- @property
- def data(self):
+ return ret
+
+ def to_representation(self, data):
"""
- Returns the serialized data on the serializer.
+ List of object instances -> List of dicts of primitive datatypes.
"""
- if self._data is None:
- obj = self.object
+ # Dealing with nested relationships, data can be a Manager,
+ # so, first get a queryset from the Manager if needed
+ iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data
+ return [
+ self.child.to_representation(item) for item in iterable
+ ]
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
- if many:
- warnings.warn('Implict list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=2)
-
- if many:
- self._data = [self.to_native(item) for item in obj]
- else:
- self._data = self.to_native(obj)
-
- return self._data
+ def validate(self, attrs):
+ return attrs
- def save_object(self, obj, **kwargs):
- obj.save(**kwargs)
+ def update(self, instance, validated_data):
+ raise NotImplementedError(
+ "Serializers with many=True do not support multiple update by "
+ "default, only multiple create. For updates it is unclear how to "
+ "deal with insertions and deletions. If you need to support "
+ "multiple update, use a `ListSerializer` class and override "
+ "`.update()` so you can specify the behavior exactly."
+ )
- def delete_object(self, obj):
- obj.delete()
+ def create(self, validated_data):
+ return [
+ self.child.create(attrs) for attrs in validated_data
+ ]
def save(self, **kwargs):
"""
- Save the deserialized object and return it.
+ Save and return a list of object instances.
"""
- if isinstance(self.object, list):
- [self.save_object(item, **kwargs) for item in self.object]
+ validated_data = [
+ dict(list(attrs.items()) + list(kwargs.items()))
+ for attrs in self.validated_data
+ ]
+
+ if self.instance is not None:
+ self.instance = self.update(self.instance, validated_data)
+ assert self.instance is not None, (
+ '`update()` did not return an object instance.'
+ )
else:
- self.save_object(self.object, **kwargs)
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
- if self.allow_add_remove and self._deleted:
- [self.delete_object(item) for item in self._deleted]
+ return self.instance
- return self.object
+ def __repr__(self):
+ return unicode_to_repr(representation.list_repr(self, indent=1))
- def metadata(self):
- """
- Return a dictionary of metadata about the fields on the serializer.
- Useful for things like responding to OPTIONS requests, or generating
- API schemas for auto-documentation.
- """
- return SortedDict(
- [(field_name, field.metadata())
- for field_name, field in six.iteritems(self.fields)]
- )
+ # Include a backlink to the serializer class on return objects.
+ # Allows renderers such as HTMLFormRenderer to get the full field info.
+
+ @property
+ def data(self):
+ ret = super(ListSerializer, self).data
+ return ReturnList(ret, serializer=self)
+ @property
+ def errors(self):
+ ret = super(ListSerializer, self).errors
+ if isinstance(ret, dict):
+ return ReturnDict(ret, serializer=self)
+ return ReturnList(ret, serializer=self)
-class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
- pass
+# ModelSerializer & HyperlinkedModelSerializer
+# --------------------------------------------
-class ModelSerializerOptions(SerializerOptions):
+def raise_errors_on_nested_writes(method_name, serializer, validated_data):
"""
- Meta class options for ModelSerializer
+ Give explicit errors when users attempt to pass writable nested data.
+
+ If we don't do this explicitly they'd get a less helpful error when
+ calling `.save()` on the serializer.
+
+ We don't *automatically* support these sorts of nested writes because
+ there are too many ambiguities to define a default behavior.
+
+ Eg. Suppose we have a `UserSerializer` with a nested profile. How should
+ we handle the case of an update, where the `profile` relationship does
+ not exist? Any of the following might be valid:
+
+ * Raise an application error.
+ * Silently ignore the nested part of the update.
+ * Automatically create a profile instance.
"""
- def __init__(self, meta):
- super(ModelSerializerOptions, self).__init__(meta)
- self.model = getattr(meta, 'model', None)
- self.read_only_fields = getattr(meta, 'read_only_fields', ())
+
+ # Ensure we don't have a writable nested field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # profile = ProfileSerializer()
+ assert not any(
+ isinstance(field, BaseSerializer) and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable nested'
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'nested serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
+
+ # Ensure we don't have a writable dotted-source field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # address = serializer.CharField('profile.address')
+ assert not any(
+ '.' in field.source and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable dotted-source '
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'dotted-source serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
class ModelSerializer(Serializer):
"""
- A serializer that deals with model instances and querysets.
- """
- _options_class = ModelSerializerOptions
+ A `ModelSerializer` is just a regular `Serializer`, except that:
+
+ * A set of default fields are automatically populated.
+ * A set of default validators are automatically populated.
+ * Default `.create()` and `.update()` implementations are provided.
+
+ The process of automatically determining a set of serializer fields
+ based on the model fields is reasonably complex, but you almost certainly
+ don't need to dig into the implementation.
- field_mapping = {
+ If the `ModelSerializer` class *doesn't* generate the set of fields that
+ you need you should either declare the extra/differing fields explicitly on
+ the serializer class, or simply use a `Serializer` class.
+ """
+ serializer_field_mapping = {
models.AutoField: IntegerField,
+ models.BigIntegerField: IntegerField,
+ models.BooleanField: BooleanField,
+ models.CharField: CharField,
+ models.CommaSeparatedIntegerField: CharField,
+ models.DateField: DateField,
+ models.DateTimeField: DateTimeField,
+ models.DecimalField: DecimalField,
+ models.EmailField: EmailField,
+ models.Field: ModelField,
+ models.FileField: FileField,
models.FloatField: FloatField,
+ models.ImageField: ImageField,
models.IntegerField: IntegerField,
+ models.NullBooleanField: NullBooleanField,
models.PositiveIntegerField: IntegerField,
- models.SmallIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
- models.DateTimeField: DateTimeField,
- models.DateField: DateField,
- models.TimeField: TimeField,
- models.DecimalField: DecimalField,
- models.EmailField: EmailField,
- models.CharField: CharField,
- models.URLField: URLField,
models.SlugField: SlugField,
+ models.SmallIntegerField: IntegerField,
models.TextField: CharField,
- models.CommaSeparatedIntegerField: CharField,
- models.BooleanField: BooleanField,
- models.FileField: FileField,
- models.ImageField: ImageField,
+ models.TimeField: TimeField,
+ models.URLField: URLField,
}
+ serializer_related_field = PrimaryKeyRelatedField
+ serializer_url_field = HyperlinkedIdentityField
+ serializer_choice_field = ChoiceField
+
+ # Default `create` and `update` behavior...
- def get_default_fields(self):
+ def create(self, validated_data):
"""
- Return all the fields that should be serialized for the model.
+ We have a bit of extra checking around this in order to provide
+ descriptive messages when something goes wrong, but this method is
+ essentially just:
+
+ return ExampleModel.objects.create(**validated_data)
+
+ If there are many to many fields present on the instance then they
+ cannot be set until the model is instantiated, in which case the
+ implementation is like so:
+
+ example_relationship = validated_data.pop('example_relationship')
+ instance = ExampleModel.objects.create(**validated_data)
+ instance.example_relationship = example_relationship
+ return instance
+
+ The default implementation also does not handle nested relationships.
+ If you want to support writable nested relationships you'll need
+ to write an explicit `.create()` method.
"""
+ raise_errors_on_nested_writes('create', self, validated_data)
- cls = self.opts.model
- assert cls is not None, \
- "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__
- opts = get_concrete_model(cls)._meta
- ret = SortedDict()
- nested = bool(self.opts.depth)
+ ModelClass = self.Meta.model
- # Deal with adding the primary key field
- pk_field = opts.pk
- while pk_field.rel and pk_field.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk
- pk_field = pk_field.rel.to._meta.pk
+ # Remove many-to-many relationships from validated_data.
+ # They are not valid arguments to the default `.create()` method,
+ # as they require that the instance has already been saved.
+ info = model_meta.get_field_info(ModelClass)
+ many_to_many = {}
+ for field_name, relation_info in info.relations.items():
+ if relation_info.to_many and (field_name in validated_data):
+ many_to_many[field_name] = validated_data.pop(field_name)
- field = self.get_pk_field(pk_field)
- if field:
- ret[pk_field.name] = field
+ try:
+ instance = ModelClass.objects.create(**validated_data)
+ except TypeError as exc:
+ msg = (
+ 'Got a `TypeError` when calling `%s.objects.create()`. '
+ 'This may be because you have a writable field on the '
+ 'serializer class that is not a valid argument to '
+ '`%s.objects.create()`. You may need to make the field '
+ 'read-only, or override the %s.create() method to handle '
+ 'this correctly.\nOriginal exception text was: %s.' %
+ (
+ ModelClass.__name__,
+ ModelClass.__name__,
+ self.__class__.__name__,
+ exc
+ )
+ )
+ raise TypeError(msg)
- # Deal with forward relationships
- forward_rels = [field for field in opts.fields if field.serialize]
- forward_rels += [field for field in opts.many_to_many if field.serialize]
+ # Save many-to-many relationships after the instance is created.
+ if many_to_many:
+ for field_name, value in many_to_many.items():
+ setattr(instance, field_name, value)
- for model_field in forward_rels:
- has_through_model = False
+ return instance
- if model_field.rel:
- to_many = isinstance(model_field,
- models.fields.related.ManyToManyField)
- related_model = model_field.rel.to
+ def update(self, instance, validated_data):
+ raise_errors_on_nested_writes('update', self, validated_data)
- if to_many and not model_field.rel.through._meta.auto_created:
- has_through_model = True
+ for attr, value in validated_data.items():
+ setattr(instance, attr, value)
+ instance.save()
- if model_field.rel and nested:
- if len(inspect.getargspec(self.get_nested_field).args) == 2:
- warnings.warn(
- 'The `get_nested_field(model_field)` call signature '
- 'is due to be deprecated. '
- 'Use `get_nested_field(model_field, related_model, '
- 'to_many) instead',
- PendingDeprecationWarning
- )
- field = self.get_nested_field(model_field)
- else:
- field = self.get_nested_field(model_field, related_model, to_many)
- elif model_field.rel:
- if len(inspect.getargspec(self.get_nested_field).args) == 3:
- warnings.warn(
- 'The `get_related_field(model_field, to_many)` call '
- 'signature is due to be deprecated. '
- 'Use `get_related_field(model_field, related_model, '
- 'to_many) instead',
- PendingDeprecationWarning
- )
- field = self.get_related_field(model_field, to_many=to_many)
- else:
- field = self.get_related_field(model_field, related_model, to_many)
- else:
- field = self.get_field(model_field)
+ return instance
- if field:
- if has_through_model:
- field.read_only = True
+ # Determine the fields to apply...
- ret[model_field.name] = field
+ def get_fields(self):
+ """
+ Return the dict of field names -> field instances that should be
+ used for `self.fields` when instantiating the serializer.
+ """
+ assert hasattr(self, 'Meta'), (
+ 'Class {serializer_class} missing "Meta" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+ assert hasattr(self.Meta, 'model'), (
+ 'Class {serializer_class} missing "Meta.model" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
- # Deal with reverse relationships
- if not self.opts.fields:
- reverse_rels = []
- else:
- # Reverse relationships are only included if they are explicitly
- # present in the `fields` option on the serializer
- reverse_rels = opts.get_all_related_objects()
- reverse_rels += opts.get_all_related_many_to_many_objects()
-
- for relation in reverse_rels:
- accessor_name = relation.get_accessor_name()
- if not self.opts.fields or accessor_name not in self.opts.fields:
+ declared_fields = copy.deepcopy(self._declared_fields)
+ model = getattr(self.Meta, 'model')
+ depth = getattr(self.Meta, 'depth', 0)
+
+ if depth is not None:
+ assert depth >= 0, "'depth' may not be negative."
+ assert depth <= 10, "'depth' may not be greater than 10."
+
+ # Retrieve metadata about fields & relationships on the model class.
+ info = model_meta.get_field_info(model)
+ field_names = self.get_field_names(declared_fields, info)
+
+ # Determine any extra field arguments and hidden fields that
+ # should be included
+ extra_kwargs = self.get_extra_kwargs()
+ extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
+ field_names, declared_fields, extra_kwargs
+ )
+
+ # Determine the fields that should be included on the serializer.
+ fields = OrderedDict()
+
+ for field_name in field_names:
+ # If the field is explicitly declared on the class then use that.
+ if field_name in declared_fields:
+ fields[field_name] = declared_fields[field_name]
continue
- related_model = relation.model
- to_many = relation.field.rel.multiple
- has_through_model = False
- is_m2m = isinstance(relation.field,
- models.fields.related.ManyToManyField)
- if is_m2m and not relation.field.rel.through._meta.auto_created:
- has_through_model = True
+ # Determine the serializer field class and keyword arguments.
+ field_class, field_kwargs = self.build_field(
+ field_name, info, model, depth
+ )
- if nested:
- field = self.get_nested_field(None, related_model, to_many)
- else:
- field = self.get_related_field(None, related_model, to_many)
-
- if field:
- if has_through_model:
- field.read_only = True
-
- ret[accessor_name] = field
-
- # Add the `read_only` flag to any fields that have bee specified
- # in the `read_only_fields` option
- for field_name in self.opts.read_only_fields:
- assert field_name not in self.base_fields.keys(), \
- "field '%s' on serializer '%s' specfied in " \
- "`read_only_fields`, but also added " \
- "as an explict field. Remove it from `read_only_fields`." % \
- (field_name, self.__class__.__name__)
- assert field_name in ret, \
- "Noexistant field '%s' specified in `read_only_fields` " \
- "on serializer '%s'." % \
- (field_name, self.__class__.__name__)
- ret[field_name].read_only = True
+ # Include any kwargs defined in `Meta.extra_kwargs`
+ extra_field_kwargs = extra_kwargs.get(field_name, {})
+ field_kwargs = self.include_extra_kwargs(
+ field_kwargs, extra_field_kwargs
+ )
- return ret
+ # Create the serializer field.
+ fields[field_name] = field_class(**field_kwargs)
- def get_pk_field(self, model_field):
+ # Add in any hidden fields.
+ fields.update(hidden_fields)
+
+ return fields
+
+ # Methods for determining the set of field names to include...
+
+ def get_field_names(self, declared_fields, info):
"""
- Returns a default instance of the pk field.
+ Returns the list of all field names that should be created when
+ instantiating this serializer class. This is based on the default
+ set of fields, but also takes into account the `Meta.fields` or
+ `Meta.exclude` options if they have been specified.
"""
- return self.get_field(model_field)
+ fields = getattr(self.Meta, 'fields', None)
+ exclude = getattr(self.Meta, 'exclude', None)
- def get_nested_field(self, model_field, related_model, to_many):
- """
- Creates a default instance of a nested relational field.
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
- Note that model_field will be `None` for reverse relationships.
- """
- class NestedModelSerializer(ModelSerializer):
- class Meta:
- model = related_model
- depth = self.opts.depth - 1
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
- return NestedModelSerializer(many=to_many)
+ assert not (fields and exclude), (
+ "Cannot set both 'fields' and 'exclude' options on "
+ "serializer {serializer_class}.".format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+
+ if fields is not None:
+ # Ensure that all declared fields have also been included in the
+ # `Meta.fields` option.
+
+ # Do not require any fields that are declared a parent class,
+ # in order to allow serializer subclasses to only include
+ # a subset of fields.
+ required_field_names = set(declared_fields)
+ for cls in self.__class__.__bases__:
+ required_field_names -= set(getattr(cls, '_declared_fields', []))
+
+ for field_name in required_field_names:
+ assert field_name in fields, (
+ "The field '{field_name}' was declared on serializer "
+ "{serializer_class}, but has not been included in the "
+ "'fields' option.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ return fields
+
+ # Use the default set of field names if `Meta.fields` is not specified.
+ fields = self.get_default_field_names(declared_fields, info)
+
+ if exclude is not None:
+ # If `Meta.exclude` is included, then remove those fields.
+ for field_name in exclude:
+ assert field_name in fields, (
+ "The field '{field_name}' was include on serializer "
+ "{serializer_class} in the 'exclude' option, but does "
+ "not match any model field.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ fields.remove(field_name)
- def get_related_field(self, model_field, related_model, to_many):
+ return fields
+
+ def get_default_field_names(self, declared_fields, model_info):
+ """
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
"""
- Creates a default instance of a flat relational field.
+ return (
+ [model_info.pk.name] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
+
+ # Methods for constructing serializer fields...
- Note that model_field will be `None` for reverse relationships.
+ def build_field(self, field_name, info, model_class, nested_depth):
"""
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
+ Return a two tuple of (cls, kwargs) to build a serializer field with.
+ """
+ if field_name in info.fields_and_pk:
+ model_field = info.fields_and_pk[field_name]
+ return self.build_standard_field(field_name, model_field)
+
+ elif field_name in info.relations:
+ relation_info = info.relations[field_name]
+ if not nested_depth:
+ return self.build_relational_field(field_name, relation_info)
+ else:
+ return self.build_nested_field(field_name, relation_info, nested_depth)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'many': to_many
- }
+ elif hasattr(model_class, field_name):
+ return self.build_property_field(field_name, model_class)
- if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ elif field_name == api_settings.URL_FIELD_NAME:
+ return self.build_url_field(field_name, model_class)
- return PrimaryKeyRelatedField(**kwargs)
+ return self.build_unknown_field(field_name, model_class)
- def get_field(self, model_field):
+ def build_standard_field(self, field_name, model_field):
"""
- Creates a default instance of a basic non-relational field.
+ Create regular model fields.
"""
- kwargs = {}
+ field_mapping = ClassLookupDict(self.serializer_field_mapping)
+
+ field_class = field_mapping[model_field]
+ field_kwargs = get_field_kwargs(field_name, model_field)
+
+ if 'choices' in field_kwargs:
+ # Fields with choices get coerced into `ChoiceField`
+ # instead of using their regular typed field.
+ field_class = self.serializer_choice_field
+
+ if not issubclass(field_class, ModelField):
+ # `model_field` is only valid for the fallback case of
+ # `ModelField`, which is used when no other typed field
+ # matched to the model field.
+ field_kwargs.pop('model_field', None)
+
+ if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField):
+ # `allow_blank` is only valid for textual fields.
+ field_kwargs.pop('allow_blank', None)
+
+ if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
+ # Populate the `child` argument on `ListField` instances generated
+ # for the PostgrSQL specfic `ArrayField`.
+ child_model_field = model_field.base_field
+ child_field_class, child_field_kwargs = self.build_standard_field(
+ 'child', child_model_field
+ )
+ field_kwargs['child'] = child_field_class(**child_field_kwargs)
- if model_field.null or model_field.blank:
- kwargs['required'] = False
+ return field_class, field_kwargs
- if isinstance(model_field, models.AutoField) or not model_field.editable:
- kwargs['read_only'] = True
+ def build_relational_field(self, field_name, relation_info):
+ """
+ Create fields for forward and reverse relationships.
+ """
+ field_class = self.serializer_related_field
+ field_kwargs = get_relation_kwargs(field_name, relation_info)
- if model_field.has_default():
- kwargs['default'] = model_field.get_default()
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_class, HyperlinkedRelatedField):
+ field_kwargs.pop('view_name', None)
- if issubclass(model_field.__class__, models.TextField):
- kwargs['widget'] = widgets.Textarea
+ return field_class, field_kwargs
- if model_field.verbose_name is not None:
- kwargs['label'] = model_field.verbose_name
+ def build_nested_field(self, field_name, relation_info, nested_depth):
+ """
+ Create nested fields for forward and reverse relationships.
+ """
+ class NestedSerializer(ModelSerializer):
+ class Meta:
+ model = relation_info.related_model
+ depth = nested_depth
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
- # TODO: TypedChoiceField?
- if model_field.flatchoices: # This ModelField contains choices
- kwargs['choices'] = model_field.flatchoices
- return ChoiceField(**kwargs)
+ return field_class, field_kwargs
- # put this below the ChoiceField because min_value isn't a valid initializer
- if issubclass(model_field.__class__, models.PositiveIntegerField) or\
- issubclass(model_field.__class__, models.PositiveSmallIntegerField):
- kwargs['min_value'] = 0
+ def build_property_field(self, field_name, model_class):
+ """
+ Create a read only field for model methods and properties.
+ """
+ field_class = ReadOnlyField
+ field_kwargs = {}
- attribute_dict = {
- models.CharField: ['max_length'],
- models.CommaSeparatedIntegerField: ['max_length'],
- models.DecimalField: ['max_digits', 'decimal_places'],
- models.EmailField: ['max_length'],
- models.FileField: ['max_length'],
- models.ImageField: ['max_length'],
- models.SlugField: ['max_length'],
- models.URLField: ['max_length'],
- }
+ return field_class, field_kwargs
- if model_field.__class__ in attribute_dict:
- attributes = attribute_dict[model_field.__class__]
- for attribute in attributes:
- kwargs.update({attribute: getattr(model_field, attribute)})
+ def build_url_field(self, field_name, model_class):
+ """
+ Create a field representing the object's own URL.
+ """
+ field_class = self.serializer_url_field
+ field_kwargs = get_url_kwargs(model_class)
- try:
- return self.field_mapping[model_field.__class__](**kwargs)
- except KeyError:
- return ModelField(model_field=model_field, **kwargs)
+ return field_class, field_kwargs
- def get_validation_exclusions(self):
+ def build_unknown_field(self, field_name, model_class):
"""
- Return a list of field names to exclude from model validation.
+ Raise an error on any unknown fields.
"""
- cls = self.opts.model
- opts = get_concrete_model(cls)._meta
- exclusions = [field.name for field in opts.fields + opts.many_to_many]
- for field_name, field in self.fields.items():
- field_name = field.source or field_name
- if field_name in exclusions and not field.read_only:
- exclusions.remove(field_name)
- return exclusions
+ raise ImproperlyConfigured(
+ 'Field name `%s` is not valid for model `%s`.' %
+ (field_name, model_class.__name__)
+ )
- def full_clean(self, instance):
+ def include_extra_kwargs(self, kwargs, extra_kwargs):
"""
- Perform Django's full_clean, and populate the `errors` dictionary
- if any validation errors occur.
+ Include any 'extra_kwargs' that have been included for this field,
+ possibly removing any incompatible existing keyword arguments.
+ """
+ if extra_kwargs.get('read_only', False):
+ for attr in [
+ 'required', 'default', 'allow_blank', 'allow_null',
+ 'min_length', 'max_length', 'min_value', 'max_value',
+ 'validators', 'queryset'
+ ]:
+ kwargs.pop(attr, None)
+
+ if extra_kwargs.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
- Note that we don't perform this inside the `.restore_object()` method,
- so that subclasses can override `.restore_object()`, and still get
- the full_clean validation checking.
+ kwargs.update(extra_kwargs)
+
+ return kwargs
+
+ # Methods for determining additional keyword arguments to apply...
+
+ def get_extra_kwargs(self):
"""
- try:
- instance.full_clean(exclude=self.get_validation_exclusions())
- except ValidationError as err:
- self._errors = err.message_dict
- return None
- return instance
+ Return a dictionary mapping field names to a dictionary of
+ additional keyword arguments.
+ """
+ extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+
+ read_only_fields = getattr(self.Meta, 'read_only_fields', None)
+ if read_only_fields is not None:
+ for field_name in read_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['read_only'] = True
+ extra_kwargs[field_name] = kwargs
+
+ # These are all pending deprecation.
+ write_only_fields = getattr(self.Meta, 'write_only_fields', None)
+ if write_only_fields is not None:
+ warnings.warn(
+ "The `Meta.write_only_fields` option is deprecated. "
+ "Use `Meta.extra_kwargs={<field_name>: {'write_only': True}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ for field_name in write_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['write_only'] = True
+ extra_kwargs[field_name] = kwargs
+
+ view_name = getattr(self.Meta, 'view_name', None)
+ if view_name is not None:
+ warnings.warn(
+ "The `Meta.view_name` option is deprecated. "
+ "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['view_name'] = view_name
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
+
+ lookup_field = getattr(self.Meta, 'lookup_field', None)
+ if lookup_field is not None:
+ warnings.warn(
+ "The `Meta.lookup_field` option is deprecated. "
+ "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['lookup_field'] = lookup_field
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
- def restore_object(self, attrs, instance=None):
+ return extra_kwargs
+
+ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
"""
- Restore the model instance.
+ Return any additional field options that need to be included as a
+ result of uniqueness constraints on the model. This is returned as
+ a two-tuple of:
+
+ ('dict of updated extra kwargs', 'mapping of hidden fields')
"""
- m2m_data = {}
- related_data = {}
- meta = self.opts.model._meta
+ model = getattr(self.Meta, 'model')
+ model_fields = self._get_model_fields(
+ field_names, declared_fields, extra_kwargs
+ )
- # Reverse fk or one-to-one relations
- for (obj, model) in meta.get_all_related_objects_with_model():
- field_name = obj.field.related_query_name()
- if field_name in attrs:
- related_data[field_name] = attrs.pop(field_name)
+ # Determine if we need any additional `HiddenField` or extra keyword
+ # arguments to deal with `unique_for` dates that are required to
+ # be in the input data in order to validate it.
+ unique_constraint_names = set()
+
+ for model_field in model_fields.values():
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names |= set([
+ model_field.unique_for_date,
+ model_field.unique_for_month,
+ model_field.unique_for_year
+ ])
+
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(field_names).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ hidden_fields = {}
+ uniqueness_extra_kwargs = {}
+
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is referred too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = unique_constraint_field.default
+ else:
+ default = empty
- # Reverse m2m relations
- for (obj, model) in meta.get_all_related_m2m_objects_with_model():
- field_name = obj.field.related_query_name()
- if field_name in attrs:
- m2m_data[field_name] = attrs.pop(field_name)
+ if unique_constraint_name in model_fields:
+ # The corresponding field is present in the serializer
+ if default is empty:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
+ else:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
+
+ # Update `extra_kwargs` with any new options.
+ for key, value in uniqueness_extra_kwargs.items():
+ if key in extra_kwargs:
+ extra_kwargs[key].update(value)
+ else:
+ extra_kwargs[key] = value
- # Forward m2m relations
- for field in meta.many_to_many:
- if field.name in attrs:
- m2m_data[field.name] = attrs.pop(field.name)
+ return extra_kwargs, hidden_fields
- # Update an existing instance...
- if instance is not None:
- for key, val in attrs.items():
- setattr(instance, key, val)
+ def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
+ """
+ Returns all the model fields that are being mapped to by fields
+ on the serializer class.
+ Returned as a dict of 'model field name' -> 'model field'.
+ Used internally by `get_uniqueness_field_options`.
+ """
+ model = getattr(self.Meta, 'model')
+ model_fields = {}
+
+ for field_name in field_names:
+ if field_name in declared_fields:
+ # If the field is declared on the serializer
+ field = declared_fields[field_name]
+ source = field.source or field_name
+ else:
+ try:
+ source = extra_kwargs[field_name]['source']
+ except KeyError:
+ source = field_name
+
+ if '.' in source or source == '*':
+ # Model fields will always have a simple source mapping,
+ # they can't be nested attribute lookups.
+ continue
- # ...or create a new instance
- else:
- instance = self.opts.model(**attrs)
+ try:
+ field = model._meta.get_field(source)
+ if isinstance(field, DjangoModelField):
+ model_fields[source] = field
+ except FieldDoesNotExist:
+ pass
- # Any relations that cannot be set until we've
- # saved the model get hidden away on these
- # private attributes, so we can deal with them
- # at the point of save.
- instance._related_data = related_data
- instance._m2m_data = m2m_data
+ return model_fields
- return instance
+ # Determine the validators to apply...
- def from_native(self, data, files):
+ def get_validators(self):
"""
- Override the default method to also include model field validation.
+ Determine the set of validators to use when instantiating serializer.
"""
- instance = super(ModelSerializer, self).from_native(data, files)
- if not self._errors:
- return self.full_clean(instance)
+ # If the validators have been declared explicitly then use that.
+ validators = getattr(getattr(self, 'Meta', None), 'validators', None)
+ if validators is not None:
+ return validators[:]
+
+ # Otherwise use the default set of validators.
+ return (
+ self.get_unique_together_validators() +
+ self.get_unique_for_date_validators()
+ )
- def save_object(self, obj, **kwargs):
+ def get_unique_together_validators(self):
"""
- Save the deserialized object and return it.
+ Determine a default set of validators for any unique_together contraints.
"""
- obj.save(**kwargs)
+ model_class_inheritance_tree = (
+ [self.Meta.model] +
+ list(self.Meta.model._meta.parents.keys())
+ )
- if getattr(obj, '_m2m_data', None):
- for accessor_name, object_list in obj._m2m_data.items():
- setattr(obj, accessor_name, object_list)
- del(obj._m2m_data)
+ # The field names we're passing though here only include fields
+ # which may map onto a model field. Any dotted field name lookups
+ # cannot map to a field, and must be a traversal, so we're not
+ # including those.
+ field_names = set([
+ field.source for field in self.fields.values()
+ if (field.source != '*') and ('.' not in field.source)
+ ])
+
+ # Note that we make sure to check `unique_together` both on the
+ # base model class, but also on any parent classes.
+ validators = []
+ for parent_class in model_class_inheritance_tree:
+ for unique_together in parent_class._meta.unique_together:
+ if field_names.issuperset(set(unique_together)):
+ validator = UniqueTogetherValidator(
+ queryset=parent_class._default_manager,
+ fields=unique_together
+ )
+ validators.append(validator)
+ return validators
- if getattr(obj, '_related_data', None):
- for accessor_name, related in obj._related_data.items():
- setattr(obj, accessor_name, related)
- del(obj._related_data)
+ def get_unique_for_date_validators(self):
+ """
+ Determine a default set of validators for the following contraints:
+ * unique_for_date
+ * unique_for_month
+ * unique_for_year
+ """
+ info = model_meta.get_field_info(self.Meta.model)
+ default_manager = self.Meta.model._default_manager
+ field_names = [field.source for field in self.fields.values()]
-class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
- """
- Options for HyperlinkedModelSerializer
- """
- def __init__(self, meta):
- super(HyperlinkedModelSerializerOptions, self).__init__(meta)
- self.view_name = getattr(meta, 'view_name', None)
- self.lookup_field = getattr(meta, 'lookup_field', None)
+ validators = []
+ for field_name, field in info.fields_and_pk.items():
+ if field.unique_for_date and field_name in field_names:
+ validator = UniqueForDateValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_date
+ )
+ validators.append(validator)
-class HyperlinkedModelSerializer(ModelSerializer):
- """
- A subclass of ModelSerializer that uses hyperlinked relationships,
- instead of primary key relationships.
- """
- _options_class = HyperlinkedModelSerializerOptions
- _default_view_name = '%(model_name)s-detail'
- _hyperlink_field_class = HyperlinkedRelatedField
+ if field.unique_for_month and field_name in field_names:
+ validator = UniqueForMonthValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_month
+ )
+ validators.append(validator)
- def get_default_fields(self):
- fields = super(HyperlinkedModelSerializer, self).get_default_fields()
+ if field.unique_for_year and field_name in field_names:
+ validator = UniqueForYearValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_year
+ )
+ validators.append(validator)
- if self.opts.view_name is None:
- self.opts.view_name = self._get_default_view_name(self.opts.model)
+ return validators
- if 'url' not in fields:
- url_field = HyperlinkedIdentityField(
- view_name=self.opts.view_name,
- lookup_field=self.opts.lookup_field
- )
- ret = self._dict_class()
- ret['url'] = url_field
- ret.update(fields)
- fields = ret
- return fields
+if hasattr(models, 'UUIDField'):
+ ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField
- def get_pk_field(self, model_field):
- if self.opts.fields and model_field.name in self.opts.fields:
- return self.get_field(model_field)
+if postgres_fields:
+ class CharMappingField(DictField):
+ child = CharField()
- def get_related_field(self, model_field, related_model, to_many):
- """
- Creates a default instance of a flat relational field.
- """
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'view_name': self._get_default_view_name(related_model),
- 'many': to_many
- }
+ ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField
+ ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField
- if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
- if self.opts.lookup_field:
- kwargs['lookup_field'] = self.opts.lookup_field
+class HyperlinkedModelSerializer(ModelSerializer):
+ """
+ A type of `ModelSerializer` that uses hyperlinked relationships instead
+ of primary key relationships. Specifically:
- return self._hyperlink_field_class(**kwargs)
+ * A 'url' field is included instead of the 'id' field.
+ * Relationships to other instances are hyperlinks, instead of primary keys.
+ """
+ serializer_related_field = HyperlinkedRelatedField
- def get_identity(self, data):
+ def get_default_field_names(self, declared_fields, model_info):
"""
- This hook is required for bulk update.
- We need to override the default, to use the url as the identity.
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
"""
- try:
- return data.get('url', None)
- except AttributeError:
- return None
+ return (
+ [api_settings.URL_FIELD_NAME] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
- def _get_default_view_name(self, model):
+ def build_nested_field(self, field_name, relation_info, nested_depth):
"""
- Return the view name to use if 'view_name' is not specified in 'Meta'
+ Create nested fields for forward and reverse relationships.
"""
- model_meta = model._meta
- format_kwargs = {
- 'app_label': model_meta.app_label,
- 'model_name': model_meta.object_name.lower()
- }
- return self._default_view_name % format_kwargs
+ class NestedSerializer(HyperlinkedModelSerializer):
+ class Meta:
+ model = relation_info.related_model
+ depth = nested_depth - 1
+
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
+
+ return field_class, field_kwargs