Deprecate SerializerMethodField in favour of typed fields with the source attribute #8251
Replies: 2 comments
-
| I would reverse the logic - for backwards compatibility reasons - so that the original function executes first. Note that in addition to the AttributeError, a SkipField exception is also caught. This is in case a field is marked 'read_only' when AttributeError is caught in the original method and a SkipField exception is thrown instead | 
Beta Was this translation helpful? Give feedback.
-
| I took the above and extended it a bit to cover my use cases. With the method given by @zhbonito if you have 'allow_null' set the field will simply return None every time since that is a fallback method in the except block for AttributeErrors. I am using 'allow_null' to reflect a required field with a potentially Null value in the API schema. To get past the potential backwards compatibility issues, I did implement a 'serializer_method_source' argument for the Field class like @stnatic proposed. That worked great except in the case of 'many=True' so I made a serializer that allows the 'many_init' function to pass the new argument to child serializers. It could probably be extended to all serializers but I am just using a custom one to be explicit where the functionality is allowed. from rest_framework.fields import Field, empty
from rest_framework.serializers import LIST_SERIALIZER_KWARGS, ListSerializer, ModelSerializer
EXTENDED_LIST_SERIALIZER_KWARGS = tuple([*LIST_SERIALIZER_KWARGS, 'serializer_method_source'])
def patch_rest_framework_field():
    """
    Patch Field's '__init__' and 'get_attribute' methods to allow
    passing serializer method fields as 'serializer_method_source'
    """
    original_method = Field.get_attribute
    original_init = Field.__init__
    def __init__(self, read_only=False, write_only=False,
                 required=None, default=empty, initial=empty, source=None,
                 label=None, help_text=None, style=None,
                 error_messages=None, validators=None, allow_null=False,
                 serializer_method_source=None):
        original_init(self, read_only=read_only, write_only=write_only,
                      required=required, default=default, initial=initial, source=source,
                      label=label, help_text=help_text, style=style,
                      error_messages=error_messages, validators=validators, allow_null=allow_null)
        self.serializer_method_source = serializer_method_source
    def get_attribute(self, instance):
        if self.serializer_method_source:
            serializer_method = getattr(self.parent, self.serializer_method_source, None)
            if serializer_method and callable(serializer_method):
                return serializer_method(instance)
        # Call the original implementation
        return original_method(self, instance)
    Field.__init__ = __init__
    Field.get_attribute = get_attribute
class MethodModelSerializer(ModelSerializer):
    """
    Patch Model Serializer to allow passing 'serializer_method_source' to children of List Serializer
    Used along with 'patch_rest_framework_field' to add serializer_method_source arg
    """
    patch_rest_framework_field()
    @classmethod
    def many_init(cls, *args, **kwargs):
        """
        Updates the below to use an extended version of "LIST_SERIALIZER_KWARGS"
        Original on Line 129 of rest_framework.serializers
        """
        allow_empty = kwargs.pop('allow_empty', None)
        child_serializer = cls(*args, **kwargs)
        list_kwargs = {
            'child': child_serializer,
        }
        if allow_empty is not None:
            list_kwargs['allow_empty'] = allow_empty
        list_kwargs.update({
            key: value for key, value in kwargs.items()
            if key in EXTENDED_LIST_SERIALIZER_KWARGS  # Use extended kwargs list
        })
        meta = getattr(cls, 'Meta', None)
        list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
        return list_serializer_class(*args, **list_kwargs)This way I can use it for model fields, or arbitrary method fields and get the correct API schema generated. class UserNameReadSerializer(MethodModelSerializer):
    test = serializers.SerializerMethodField()
    class Meta:
        model = User
        fields = ['id', 'first_name', 'last_name', 'test']
    @staticmethod
    def get_test():
        return 'TEST'
class PartnerDetailSerializer(serializers.ModelSerializer):
    active_credentials = serializers.SerializerMethodField()
    partner_XX = UserNameReadSerializer(serializer_method_source='get_partner_XX', allow_null=True)
    partner_YY = UserNameReadSerializer(serializer_method_source='get_partner_YY', allow_null=True)
    partner_employees = UserNameReadSerializer(serializer_method_source='get_partner_employees',
                                               allow_null=True,
                                               many=True)
    class Meta:
        model = Partner
        fields = ['id', 'active_credentials', 'partner_XX', 'partner_YY', 'partner_employees']
        read_only_fields = ['id', 'active_credentials']
    @staticmethod
    def get_active_credentials(obj) -> bool:
        if obj.credentials.filter(marketplace=Marketplaces.US.value).first().token:
            return True
        else:
            return False
    @staticmethod
    def get_partner_XX(obj: Partner):
        return User.objects.filter(
            Q(user_role__role__name=Roles.XX.value) &
            Q(user_partner__partner=obj)
        ).first()
    @staticmethod
    def get_partner_YY(obj: Partner):
        return User.objects.filter(
            Q(user_role__role__name=Roles.YY.value) &
            Q(user_partner__partner=obj)
        ).first()
    @staticmethod
    def get_partner_employees(obj: Partner):
        return User.objects.filter(
            Q(user_role__role__name=Roles.EMPLOYEE.value) &
            Q(user_partner__partner=obj)
        ).all() | 
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm often facing an issue where I would like to use a
SerializerMethodFieldin order to transform model data into a different representation. However, this does not integrate at all with libraries automatically generating the OpenAPI schema (drf-spectacular,drf-yasg).I'll use an abstract example of nested objects with an integer field:
The api schema for
Boxwill indicate thatcontentsis a string.I would prefer to use the following syntax:
This way DRF "knows" that the schema for a
Boxinstance is{"contents": {"value": int}}Unfortunately this is not possible, sincesourcecan only refer to an instance method and not to a serializer method.I've came up with a quick monkeypatch that adds this feature to the base
Fieldclass and it seems to do the trick. Is there any reason whysourcecouldn't accept serializer methods up to this point? This could be potentially implemented as a non-breaking change by adding a new field arg calledsource_serializer_method.Beta Was this translation helpful? Give feedback.
All reactions