diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 96063bd81..dd4ea8a90 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -578,16 +578,6 @@ Alternative representations include serializing using hyperlinks, serializing co 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 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. +## 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 child’s `Meta`, if it exists, otherwise the `Meta` of the first parent, etc. +* It’s 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 won’t 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 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. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 02c24b70e..3556447bb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -305,7 +305,11 @@ class SerializerMetaclass(type): # 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 + fields = [ + (field_name, obj) for field_name, obj + in base._declared_fields.items() + if field_name not in attrs + ] + fields return OrderedDict(fields) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 8c8b5b163..47258fdd1 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -7,6 +7,8 @@ import re import pytest +from django.db import models + from rest_framework import fields, relations, serializers from rest_framework.compat import unicode_repr from rest_framework.fields import Field @@ -413,3 +415,42 @@ class Test4606Regression: serializer = self.Serializer(data=[{"name": "liz"}], many=True) with pytest.raises(serializers.ValidationError): 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