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.py701
1 files changed, 587 insertions, 114 deletions
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d2740fc2..f7aa3a7d 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,20 +10,31 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
-from django.core.exceptions import ImproperlyConfigured, ValidationError
+from django.core.exceptions import ImproperlyConfigured
from django.db import models
+from django.db.models.fields import FieldDoesNotExist
from django.utils import six
-from django.utils.datastructures import SortedDict
-from collections import namedtuple
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.compat import OrderedDict
+from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
from rest_framework.utils import html, model_meta, representation
from rest_framework.utils.field_mapping import (
get_url_kwargs, get_field_kwargs,
get_relation_kwargs, get_nested_relation_kwargs,
- lookup_class
+ ClassLookupDict
+)
+from rest_framework.utils.serializer_helpers import (
+ ReturnDict, ReturnList, BoundField, NestedBoundField, BindingDict
+)
+from rest_framework.validators import (
+ UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
+ UniqueTogetherValidator
)
import copy
+import inspect
+import warnings
# Note: We do the following so that users of the framework can use this style:
#
@@ -36,19 +47,64 @@ from rest_framework.relations import * # NOQA
from rest_framework.fields import * # NOQA
-FieldResult = namedtuple('FieldResult', ['field', 'value', 'error'])
+# 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'
+)
+# BaseSerializer
+# --------------
+
class BaseSerializer(Field):
"""
The BaseSerializer class provides a minimal class which may be used
for writing custom serializer implementations.
"""
-
def __init__(self, instance=None, data=None, **kwargs):
- super(BaseSerializer, self).__init__(**kwargs)
self.instance = instance
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):
+ """
+ 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)
+ """
+ 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.')
@@ -56,31 +112,52 @@ class BaseSerializer(Field):
def to_representation(self, instance):
raise NotImplementedError('`to_representation()` must be implemented.')
- def update(self, instance, attrs):
+ def update(self, instance, validated_data):
raise NotImplementedError('`update()` must be implemented.')
- def create(self, attrs):
+ def create(self, validated_data):
raise NotImplementedError('`create()` must be implemented.')
- def save(self, extras=None):
- attrs = self.validated_data
- if extras is not None:
- attrs = dict(list(attrs.items()) + list(extras.items()))
+ 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__)
+ )
+
+ validated_data = dict(
+ list(self.validated_data.items()) +
+ list(kwargs.items())
+ )
if self.instance is not None:
- self.update(self.instance, attrs)
+ 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(attrs)
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
return self.instance
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__)
+ )
+
if not hasattr(self, '_validated_data'):
try:
- self._validated_data = self.to_internal_value(self._initial_data)
+ self._validated_data = self.run_validation(self._initial_data)
except ValidationError as exc:
self._validated_data = {}
- self._errors = exc.message_dict
+ self._errors = exc.detail
else:
self._errors = {}
@@ -92,13 +169,10 @@ class BaseSerializer(Field):
@property
def data(self):
if not hasattr(self, '_data'):
- if self.instance is not None:
+ if self.instance is not None and not getattr(self, '_errors', None):
self._data = self.to_representation(self.instance)
- elif self._initial_data is not None:
- self._data = dict([
- (field_name, field.get_value(self._initial_data))
- for field_name, field in self.fields.items()
- ])
+ 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
@@ -118,6 +192,9 @@ class BaseSerializer(Field):
return self._validated_data
+# Serializer & ListSerializer classes
+# -----------------------------------
+
class SerializerMetaclass(type):
"""
This metaclass sets a dictionary named `base_fields` on the class.
@@ -141,7 +218,7 @@ class SerializerMetaclass(type):
if hasattr(base, '_declared_fields'):
fields = list(base._declared_fields.items()) + fields
- return SortedDict(fields)
+ return OrderedDict(fields)
def __new__(cls, name, bases, attrs):
attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
@@ -150,44 +227,53 @@ class SerializerMetaclass(type):
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
- def __init__(self, *args, **kwargs):
- self.context = kwargs.pop('context', {})
- kwargs.pop('partial', None)
- kwargs.pop('many', None)
-
- super(Serializer, self).__init__(*args, **kwargs)
+ default_error_messages = {
+ 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
+ }
+ @property
+ def fields(self):
+ """
+ A dictionary of {field_name: field_instance}.
+ """
+ # `fields` is evalutated 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 get_fields(self):
+ """
+ Returns a dictionary of {field_name: field_instance}.
+ """
# 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.
- self.fields = self._get_base_fields()
-
- # Setup all the child fields, to provide them with the current context.
- for field_name, field in self.fields.items():
- field.bind(field_name, self, self)
-
- 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):
- kwargs['child'] = cls()
- return ListSerializer(*args, **kwargs)
- return super(Serializer, cls).__new__(cls, *args, **kwargs)
-
- def _get_base_fields(self):
return copy.deepcopy(self._declared_fields)
- def bind(self, field_name, parent, root):
- # If the serializer is used as a field then when it becomes bound
- # it also needs to bind all its child fields.
- super(Serializer, self).bind(field_name, parent, root)
- for field_name, field in self.fields.items():
- field.bind(field_name, self, root)
+ def get_validators(self):
+ """
+ Returns a list of validator callables.
+ """
+ # Used by the lazily-evaluated `validators` property.
+ return getattr(getattr(self, 'Meta', None), 'validators', [])
def get_initial(self):
- return dict([
+ if self._initial_data is not None:
+ 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):
@@ -197,18 +283,66 @@ class Serializer(BaseSerializer):
return html.parse_html_dict(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
- def to_internal_value(self, data):
+ def run_validation(self, data=empty):
"""
- Dict of native values <- Dict of primitive datatypes.
+ 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.
"""
+ if data is empty:
+ if getattr(self.root, 'partial', False):
+ raise SkipField()
+ if self.required:
+ self.fail('required')
+ return self.get_default()
+
+ if data is None:
+ if not self.allow_null:
+ self.fail('null')
+ return None
+
if not isinstance(data, dict):
+ message = self.error_messages['invalid'].format(
+ datatype=type(data).__name__
+ )
raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data']
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
})
- ret = {}
- errors = {}
- fields = [field for field in self.fields.values() if not field.read_only]
+ 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 as exc:
+ if isinstance(exc.detail, dict):
+ # .validate() errors may be a dict, in which case, use
+ # standard {key: list of values} style.
+ raise ValidationError(dict([
+ (key, value if isinstance(value, list) else [value])
+ for key, value in exc.detail.items()
+ ]))
+ elif isinstance(exc.detail, list):
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+ })
+ else:
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+ })
+
+ return value
+
+ def to_internal_value(self, data):
+ """
+ Dict of native values <- Dict of primitive datatypes.
+ """
+ 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)
@@ -218,7 +352,7 @@ class Serializer(BaseSerializer):
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
- errors[field.field_name] = exc.messages
+ errors[field.field_name] = exc.detail
except SkipField:
pass
else:
@@ -227,63 +361,90 @@ class Serializer(BaseSerializer):
if errors:
raise ValidationError(errors)
- try:
- return self.validate(ret)
- except ValidationError as exc:
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: exc.messages
- })
+ return ret
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
- ret = SortedDict()
+ ret = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
- native_value = field.get_attribute(instance)
- ret[field.field_name] = field.to_representation(native_value)
+ attribute = field.get_attribute(instance)
+ if attribute is None:
+ ret[field.field_name] = None
+ else:
+ ret[field.field_name] = field.to_representation(attribute)
return ret
def validate(self, attrs):
return attrs
+ def __repr__(self):
+ return representation.serializer_repr(self, indent=1)
+
+ # 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.
+
def __iter__(self):
- errors = self.errors if hasattr(self, '_errors') else {}
for field in self.fields.values():
- value = self.data.get(field.field_name) if self.data else None
- error = errors.get(field.field_name)
- yield FieldResult(field, value, error)
+ yield self[field.field_name]
- def __repr__(self):
- return representation.serializer_repr(self, indent=1)
+ 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)
+ # 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(Serializer, self).data
+ return ReturnDict(ret, serializer=self)
+
+ @property
+ def errors(self):
+ ret = super(Serializer, self).errors
+ return ReturnDict(ret, serializer=self)
+
+
+# There's some replication of `ListField` here,
+# but that's probably better than obfuscating the call hierarchy.
class ListSerializer(BaseSerializer):
child = None
- initial = []
+ many = True
+
+ default_error_messages = {
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ }
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.'
- self.context = kwargs.pop('context', {})
- kwargs.pop('partial', None)
-
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
super(ListSerializer, self).__init__(*args, **kwargs)
- self.child.bind('', self, self)
+ self.child.bind(field_name='', parent=self)
- def bind(self, field_name, parent, root):
- # If the list is used as a field then it needs to provide
- # the current context to the child serializer.
- super(ListSerializer, self).bind(field_name, parent, root)
- self.child.bind(field_name, self, root)
+ def get_initial(self):
+ if self._initial_data is not None:
+ return self.to_representation(self._initial_data)
+ return []
def get_value(self, dictionary):
+ """
+ Given the input dictionary, return the field value.
+ """
# We override the default field access in order to support
# lists in HTML forms.
- if is_html_input(dictionary):
+ if html.is_html_input(dictionary):
return html.parse_html_list(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
@@ -294,29 +455,107 @@ class ListSerializer(BaseSerializer):
if html.is_html_input(data):
data = html.parse_html_list(data)
- return [self.child.run_validation(item) for item in data]
+ 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]
+ })
+
+ ret = []
+ 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({})
+
+ if any(errors):
+ raise ValidationError(errors)
+
+ return ret
def to_representation(self, data):
"""
List of object instances -> List of dicts of primitive datatypes.
"""
- return [self.child.to_representation(item) for item in data]
+ iterable = data.all() if (hasattr(data, 'all')) else data
+ return [
+ self.child.to_representation(item) for item in iterable
+ ]
+
+ 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 create(self, attrs_list):
- return [self.child.create(attrs) for attrs in attrs_list]
+ def create(self, validated_data):
+ return [
+ self.child.create(attrs) for attrs in validated_data
+ ]
+
+ def save(self, **kwargs):
+ """
+ Save and return a list of object instances.
+ """
+ validated_data = [
+ dict(list(attrs.items()) + list(kwargs.items()))
+ for attrs in self.validated_data
+ ]
- def save(self):
if self.instance is not None:
- self.update(self.instance, self.validated_data)
- self.instance = self.create(self.validated_data)
+ 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
def __repr__(self):
return representation.list_repr(self, indent=1)
+ # 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)
+
+
+# ModelSerializer & HyperlinkedModelSerializer
+# --------------------------------------------
class ModelSerializer(Serializer):
- _field_mapping = {
+ """
+ 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.
+ """
+ _field_mapping = ClassLookupDict({
models.AutoField: IntegerField,
models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField,
@@ -331,7 +570,7 @@ class ModelSerializer(Serializer):
models.FloatField: FloatField,
models.ImageField: ImageField,
models.IntegerField: IntegerField,
- models.NullBooleanField: BooleanField,
+ models.NullBooleanField: NullBooleanField,
models.PositiveIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
models.SlugField: SlugField,
@@ -339,22 +578,35 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
- }
+ })
_related_class = PrimaryKeyRelatedField
- def create(self, attrs):
+ def create(self, validated_attrs):
+ # Check that the user isn't trying to handle a writable nested field.
+ # If we don't do this explicitly they'd likely get a confusing
+ # error at the point of calling `Model.objects.create()`.
+ assert not any(
+ isinstance(field, BaseSerializer) and not field.read_only
+ for field in self.fields.values()
+ ), (
+ 'The `.create()` method does not suport nested writable fields '
+ 'by default. Write an explicit `.create()` method for serializer '
+ '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
+
ModelClass = self.Meta.model
- # Remove many-to-many relationships from attrs.
+ # Remove many-to-many relationships from validated_attrs.
# 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 attrs):
- many_to_many[field_name] = attrs.pop(field_name)
+ if relation_info.to_many and (field_name in validated_attrs):
+ many_to_many[field_name] = validated_attrs.pop(field_name)
- instance = ModelClass.objects.create(**attrs)
+ instance = ModelClass.objects.create(**validated_attrs)
# Save many-to-many relationships after the instance is created.
if many_to_many:
@@ -363,42 +615,185 @@ class ModelSerializer(Serializer):
return instance
- def update(self, obj, attrs):
- for attr, value in attrs.items():
- setattr(obj, attr, value)
- obj.save()
+ def update(self, instance, validated_attrs):
+ assert not any(
+ isinstance(field, BaseSerializer) and not field.read_only
+ for field in self.fields.values()
+ ), (
+ 'The `.update()` method does not suport nested writable fields '
+ 'by default. Write an explicit `.update()` method for serializer '
+ '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
+
+ for attr, value in validated_attrs.items():
+ setattr(instance, attr, value)
+ instance.save()
+ return instance
+
+ def get_validators(self):
+ field_names = set([
+ field.source for field in self.fields.values()
+ if (field.source != '*') and ('.' not in field.source)
+ ])
- def _get_base_fields(self):
+ validators = getattr(getattr(self, 'Meta', None), 'validators', [])
+ model_class = self.Meta.model
+
+ # Note that we make sure to check `unique_together` both on the
+ # base model class, but also on any parent classes.
+ for parent_class in [model_class] + list(model_class._meta.parents.keys()):
+ 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)
+
+ # Add any unique_for_date/unique_for_month/unique_for_year constraints.
+ info = model_meta.get_field_info(model_class)
+ for field_name, field in info.fields_and_pk.items():
+ if field.unique_for_date and field_name in field_names:
+ validator = UniqueForDateValidator(
+ queryset=model_class._default_manager,
+ field=field_name,
+ date_field=field.unique_for_date
+ )
+ validators.append(validator)
+
+ if field.unique_for_month and field_name in field_names:
+ validator = UniqueForMonthValidator(
+ queryset=model_class._default_manager,
+ field=field_name,
+ date_field=field.unique_for_month
+ )
+ validators.append(validator)
+
+ if field.unique_for_year and field_name in field_names:
+ validator = UniqueForYearValidator(
+ queryset=model_class._default_manager,
+ field=field_name,
+ date_field=field.unique_for_year
+ )
+ validators.append(validator)
+
+ return validators
+
+ def get_fields(self):
declared_fields = copy.deepcopy(self._declared_fields)
- ret = SortedDict()
+ ret = OrderedDict()
model = getattr(self.Meta, 'model')
fields = getattr(self.Meta, 'fields', None)
+ exclude = getattr(self.Meta, 'exclude', None)
depth = getattr(self.Meta, 'depth', 0)
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+ assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'."
+
+ extra_kwargs = self._include_additional_options(extra_kwargs)
+
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
- # Use the default set of fields if none is supplied explicitly.
+ # Use the default set of field names if none is supplied explicitly.
if fields is None:
fields = self._get_default_field_names(declared_fields, info)
+ exclude = getattr(self.Meta, 'exclude', None)
+ if exclude is not None:
+ for field_name in exclude:
+ fields.remove(field_name)
+
+ # Determine the set of model fields, and the fields that they map to.
+ # We actually only need this to deal with the slightly awkward case
+ # of supporting `unique_for_date`/`unique_for_month`/`unique_for_year`.
+ model_field_mapping = {}
+ for field_name in fields:
+ if field_name in declared_fields:
+ field = declared_fields[field_name]
+ source = field.source or field_name
+ else:
+ try:
+ source = extra_kwargs[field_name]['source']
+ except KeyError:
+ source = field_name
+ # Model fields will always have a simple source mapping,
+ # they can't be nested attribute lookups.
+ if '.' not in source and source != '*':
+ model_field_mapping[source] = 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.
+ hidden_fields = {}
+ unique_constraint_names = set()
+
+ for model_field_name, field_name in model_field_mapping.items():
+ try:
+ model_field = model._meta.get_field(model_field_name)
+ except FieldDoesNotExist:
+ continue
+ # 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(fields).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.
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is refered 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
+
+ if unique_constraint_name in model_field_mapping:
+ # The corresponding field is present in the serializer
+ if unique_constraint_name not in extra_kwargs:
+ extra_kwargs[unique_constraint_name] = {}
+ if default is empty:
+ if 'required' not in extra_kwargs[unique_constraint_name]:
+ extra_kwargs[unique_constraint_name]['required'] = True
+ else:
+ if 'default' not in extra_kwargs[unique_constraint_name]:
+ 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)
+
+ # Now determine the fields that should be included on the serializer.
for field_name in fields:
if field_name in declared_fields:
# Field is explicitly declared on the class, use that.
ret[field_name] = declared_fields[field_name]
continue
- elif field_name == api_settings.URL_FIELD_NAME:
- # Create the URL field.
- field_cls = HyperlinkedIdentityField
- kwargs = get_url_kwargs(model)
-
elif field_name in info.fields_and_pk:
# Create regular model fields.
model_field = info.fields_and_pk[field_name]
- field_cls = lookup_class(self._field_mapping, model_field)
+ field_cls = self._field_mapping[model_field]
kwargs = get_field_kwargs(field_name, model_field)
if 'choices' in kwargs:
# Fields with choices get coerced into `ChoiceField`
@@ -409,6 +804,9 @@ class ModelSerializer(Serializer):
# `ModelField`, which is used when no other typed field
# matched to the model field.
kwargs.pop('model_field', None)
+ if not issubclass(field_cls, CharField):
+ # `allow_blank` is only valid for textual fields.
+ kwargs.pop('allow_blank', None)
elif field_name in info.relations:
# Create forward and reverse relationships.
@@ -428,6 +826,11 @@ class ModelSerializer(Serializer):
field_cls = ReadOnlyField
kwargs = {}
+ elif field_name == api_settings.URL_FIELD_NAME:
+ # Create the URL field.
+ field_cls = HyperlinkedIdentityField
+ kwargs = get_url_kwargs(model)
+
else:
raise ImproperlyConfigured(
'Field name `%s` is not valid for model `%s`.' %
@@ -446,13 +849,76 @@ class ModelSerializer(Serializer):
)
# Populate any kwargs defined in `Meta.extra_kwargs`
- kwargs.update(extra_kwargs.get(field_name, {}))
+ extras = extra_kwargs.get(field_name, {})
+ if extras.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 extras.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
+
+ kwargs.update(extras)
# Create the serializer field.
ret[field_name] = field_cls(**kwargs)
+ for field_name, field in hidden_fields.items():
+ ret[field_name] = field
+
return ret
+ def _include_additional_options(self, 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 pending deprecation. "
+ "Use `Meta.extra_kwargs={<field_name>: {'write_only': True}}` instead.",
+ PendingDeprecationWarning,
+ 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 pending deprecation. "
+ "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
+ PendingDeprecationWarning,
+ 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 pending deprecation. "
+ "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
+ PendingDeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['lookup_field'] = lookup_field
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
+
+ return extra_kwargs
+
def _get_default_field_names(self, declared_fields, model_info):
return (
[model_info.pk.name] +
@@ -470,6 +936,13 @@ class ModelSerializer(Serializer):
class HyperlinkedModelSerializer(ModelSerializer):
+ """
+ A type of `ModelSerializer` that uses hyperlinked relationships instead
+ of primary key relationships. Specifically:
+
+ * A 'url' field is included instead of the 'id' field.
+ * Relationships to other instances are hyperlinks, instead of primary keys.
+ """
_related_class = HyperlinkedRelatedField
def _get_default_field_names(self, declared_fields, model_info):