Add disabling of declared fields on serializer subclasses (#4764)

* Add test for disabling declared fields on child
* Check that declared base field is not in attrs
* Update meta inheritance docs to include serializer
* Test that meta fields cannot be declared as None
* Add docs example for declarative field disabling
This commit is contained in:
Ryan P Kilby 2017-01-03 16:14:19 -05:00 committed by Tom Christie
parent 8579683170
commit 11fd3bf108
3 changed files with 80 additions and 11 deletions

View File

@ -578,16 +578,6 @@ Alternative representations include serializing using hyperlinks, serializing co
For full details see the [serializer relations][relations] documentation. For full details see the [serializer relations][relations] documentation.
## Inheritance of the 'Meta' class
The inner `Meta` class on serializers is not inherited from parent classes by default. This is the same behavior as with Django's `Model` and `ModelForm` classes. If you want the `Meta` class to inherit from a parent class you must do so explicitly. For example:
class AccountSerializer(MyBaseSerializer):
class Meta(MyBaseSerializer.Meta):
model = Account
Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly.
## Customizing field mappings ## Customizing field mappings
The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer. The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer.
@ -1025,6 +1015,40 @@ If any of the validation fails, then the method should raise a `serializers.Vali
The `data` argument passed to this method will normally be the value of `request.data`, so the datatype it provides will depend on the parser classes you have configured for your API. The `data` argument passed to this method will normally be the value of `request.data`, so the datatype it provides will depend on the parser classes you have configured for your API.
## Serializer Inheritance
Similar to Django forms, you can extend and reuse serializers through inheritance. This allows you to declare a common set of fields or methods on a parent class that can then be used in a number of serializers. For example,
class MyBaseSerializer(Serializer):
my_field = serializers.CharField()
def validate_my_field(self):
...
class MySerializer(MyBaseSerializer):
...
Like Django's `Model` and `ModelForm` classes, the inner `Meta` class on serializers does not implicitly inherit from it's parents' inner `Meta` classes. If you want the `Meta` class to inherit from a parent class you must do so explicitly. For example:
class AccountSerializer(MyBaseSerializer):
class Meta(MyBaseSerializer.Meta):
model = Account
Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly.
Additionally, the following caveats apply to serializer inheritance:
* Normal Python name resolution rules apply. If you have multiple base classes that declare a `Meta` inner class, only the first one will be used. This means the childs `Meta`, if it exists, otherwise the `Meta` of the first parent, etc.
* Its possible to declaratively remove a `Field` inherited from a parent class by setting the name to be `None` on the subclass.
class MyBaseSerializer(ModelSerializer):
my_field = serializers.CharField()
class MySerializer(MyBaseSerializer):
my_field = None
However, you can only use this technique to opt out from a field defined declaratively by a parent class; it wont prevent the `ModelSerializer` from generating a default field. To opt-out from default fields, see [Specifying which fields to include](#specifying-which-fields-to-include).
## Dynamically modifying fields ## Dynamically modifying fields
Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer. Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer.

View File

@ -305,7 +305,11 @@ class SerializerMetaclass(type):
# in order to maintain the correct order of fields. # in order to maintain the correct order of fields.
for base in reversed(bases): for base in reversed(bases):
if hasattr(base, '_declared_fields'): if hasattr(base, '_declared_fields'):
fields = list(base._declared_fields.items()) + fields fields = [
(field_name, obj) for field_name, obj
in base._declared_fields.items()
if field_name not in attrs
] + fields
return OrderedDict(fields) return OrderedDict(fields)

View File

@ -7,6 +7,8 @@ import re
import pytest import pytest
from django.db import models
from rest_framework import fields, relations, serializers from rest_framework import fields, relations, serializers
from rest_framework.compat import unicode_repr from rest_framework.compat import unicode_repr
from rest_framework.fields import Field from rest_framework.fields import Field
@ -413,3 +415,42 @@ class Test4606Regression:
serializer = self.Serializer(data=[{"name": "liz"}], many=True) serializer = self.Serializer(data=[{"name": "liz"}], many=True)
with pytest.raises(serializers.ValidationError): with pytest.raises(serializers.ValidationError):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
class TestDeclaredFieldInheritance:
def test_declared_field_disabling(self):
class Parent(serializers.Serializer):
f1 = serializers.CharField()
f2 = serializers.CharField()
class Child(Parent):
f1 = None
class Grandchild(Child):
pass
assert len(Parent._declared_fields) == 2
assert len(Child._declared_fields) == 1
assert len(Grandchild._declared_fields) == 1
def test_meta_field_disabling(self):
# Declaratively setting a field on a child class will *not* prevent
# the ModelSerializer from generating a default field.
class MyModel(models.Model):
f1 = models.CharField(max_length=10)
f2 = models.CharField(max_length=10)
class Parent(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ['f1', 'f2']
class Child(Parent):
f1 = None
class Grandchild(Child):
pass
assert len(Parent().get_fields()) == 2
assert len(Child().get_fields()) == 2
assert len(Grandchild().get_fields()) == 2