4

Is there a way to pass paremeters to a Django Rest Framework's SerializerMethodField?

Assume I have the models:

class Owner(models.Model):
    name = models.CharField(max_length=10)

class Item(models.Model):
    name = models.CharField(max_length=10)
    owner = models.ForeignKey('Owner', related_name='items')
    itemType = models.CharField(max_length=5) # either "type1" or "type2"

What I need is to return an Owner JSON object with the fields: name, type1items, type2items.

My current solution is this:

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Item
        fields = ('name', 'itemType')

class OwnerSerializer(serializers.ModelSerializer):
    type1items = serializers.SerializerMethodField(method_name='getType1Items')
    type2items = serializers.SerializerMethodField(method_name='getType2Items')

    class Meta:
        model = models.Owner
        fields = ('name', 'type1items', 'type2items')

    def getType1Items(self, ownerObj):
        queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type1")
        return ItemSerializer(queryset, many=True).data

    def getType2Items(self, ownerObj):
        queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type2")
        return ItemSerializer(queryset, many=True).data

This works. But it would be much cleaner if I could pass a parameter to the method instead of using two methods with almost the exact code. Ideally it would look like this:

...
class OwnerSerializer(serializers.ModelSerializer):
    type1items = serializers.SerializerMethodField(method_name='getItems', "type1")
    type2items = serializers.SerializerMethodField(method_name='getItems', "type2")

    class Meta:
        model = models.Owner
        fields = ('name', 'type1items', 'type2items')

    def getItems(self, ownerObj, itemType):
        queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
        return ItemSerializer(queryset, many=True).data

In the docs SerializerMethodField accepts only one parameter which is method_name.

Is there any way to achieve this behaviour using SerializerMethodField? (The example code here is overly simplified so there might be mistakes.)

Bihan Viranga
  • 196
  • 2
  • 6
  • 2
    You could create a custom SerializerMethodField and override it for your needs. Another solution would be to call a common function from `getType1Items()` and `getType2Items()`, like `def _get_items(self, obj, type)`. – wfehr Apr 29 '20 at 11:14
  • Don't you mean call `getType1Items()` and `getType2Items()` from a common `_get_items` function ? That would work. – Bihan Viranga Apr 30 '20 at 09:15

2 Answers2

5

There is no way to do this with the base field.

You need to write a custom serializer field to support it. Here is an example one, which you'll probably want to modify depending on how you use it.

This version uses the kwargs from the field to pass as args to the function. I'd recommend doing this rather than using *args since you'll get more sensible errors, and flexibility in how you write your function/field definitions.

class MethodField(SerializerMethodField):
    def __init__(self, method_name=None, **kwargs):
        # use kwargs for our function instead, not the base class
        super().__init__(method_name) 
        self.func_kwargs = kwargs

    def to_representation(self, value):
        method = getattr(self.parent, self.method_name)
        return method(value, **self.func_kwargs)

Using the field in a serializer:

class Simple(Serializer):
    field = MethodField("get_val", name="sam")
    def get_val(self, obj, name=""):
        return "my name is " + name

>>> print(Simple(instance=object()).data)
{'field': 'my name is sam'}
Andrew
  • 8,322
  • 2
  • 47
  • 70
  • I wonder if there's something wrong with the way I approached the problem, because the default serializers don't cover it. Anyway, your answer is what I was looking for. Many thanks @andrew-backer ! – Bihan Viranga Apr 30 '20 at 09:14
1

You could just refactor what you have:

class OwnerSerializer(serializers.ModelSerializer):

    type1items = serializers.SerializerMethodField(method_name='getType1Items')
    type2items = serializers.SerializerMethodField(method_name='getType2Items')

    class Meta:
        model = models.Owner
        fields = ('name', 'type1items', 'type2items')

    def getType1Items(self, ownerObj):
        return getItems(ownerObj,"type1")

    def getType2Items(self, ownerObj):
        return getItems(ownerObj,"type2")

    def getItems(self, ownerObj, itemType):
        queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
        return ItemSerializer(queryset, many=True).data
Matthew Wilcoxson
  • 3,432
  • 1
  • 43
  • 48