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
SerializerMethodField
in 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
Box
will indicate thatcontents
is a string.I would prefer to use the following syntax:
This way DRF "knows" that the schema for a
Box
instance is{"contents": {"value": int}}
Unfortunately this is not possible, sincesource
can 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
Field
class and it seems to do the trick. Is there any reason whysource
couldn'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