diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0f355c76d2..9e2e53d97f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -580,6 +580,22 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu Please keep in mind that, if the field has already been explicitly declared on the serializer class, then the `extra_kwargs` option will be ignored. +It is also possible to create new serializer fields from any related model fields using the `extra_kwargs` option. For example: + + class UserProfile(models.Model): + birthdate = models.DateField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ['date_of_birth', 'first_name', 'last_name'] + extra_kwargs = { + 'date_of_birth': {'source': 'birthdate'}, + 'first_name': {'source': 'user.first_name'}, + 'last_name': {'source': 'user.last_name'} + } + ## Relational fields When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 77c181b6cc..274017fdb4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1117,9 +1117,36 @@ def get_fields(self): if source == '*': source = field_name + # Get the right model and info for source with attributes + source_attrs = source.split('.') + source_info = info + source_model = model + + if len(source_attrs) > 1: + attr_info = info + attr_model = model + + for attr in source_attrs[:-1]: + if attr not in attr_info.relations: + break + + attr_model = attr_info.relations[attr].related_model + attr_info = model_meta.get_field_info(attr_model) + else: + attr = source_attrs[-1] + if ( + attr in attr_info.fields_and_pk + or attr in attr_info.relations + or hasattr(attr_model, attr) + or attr == self.url_field_name + ): + source = attr + source_info = attr_info + source_model = attr_model + # Determine the serializer field class and keyword arguments. field_class, field_kwargs = self.build_field( - source, info, model, depth + source, source_info, source_model, depth ) # Include any kwargs defined in `Meta.extra_kwargs` diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 20d0319fcb..6178c85fed 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -13,6 +13,7 @@ import django import pytest +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import ( @@ -735,6 +736,46 @@ class Meta: """) self.assertEqual(repr(TestSerializer()), expected) + def test_source_with_attributes(self): + class UserProfile(models.Model): + age = models.IntegerField() + birthdate = models.DateField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ('username', 'email', 'first_name', 'last_name', 'age', 'birthdate') + extra_kwargs = { + 'username': { + 'source': 'user.username', + }, + 'email': { + 'source': 'user.email', + }, + 'first_name': { + 'source': 'user.first_name', + }, + 'last_name': { + 'source': 'user.last_name', + } + } + + # In Django 3.0, the maximum length of first_name is 30, whereas it is 150 + # in later versions, so we can't hard-code the value in the expected variable. + max_length = User.first_name.field.max_length + + expected = dedent(f""" + UserProfileSerializer(): + username = CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, source='user.username', validators=[, ]) + email = EmailField(allow_blank=True, label='Email address', max_length=254, required=False, source='user.email') + first_name = CharField(allow_blank=True, max_length={max_length}, required=False, source='user.first_name') + last_name = CharField(allow_blank=True, max_length=150, required=False, source='user.last_name') + age = IntegerField() + birthdate = DateField() + """) + self.assertEqual(repr(UserProfileSerializer()), expected) + class DisplayValueTargetModel(models.Model): name = models.CharField(max_length=100)