0

I have this M2M relation with through model as

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

    def __str__(self):
        return self.name


class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

Please note that, I have extra fields date_joined and invite_reason in the through model.

Now, I want to serialize the Group queryset using DRF and thus I choose the below serializer setup.

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = "__all__"


class GroupSerializer(serializers.ModelSerializer):
    members = PersonSerializer(read_only=True, many=True)

    class Meta:
        model = Group
        fields = "__all__"

and it is returning the following response,

[
    {
        "id": 1,
        "members": [
            {
                "id": 1,
                "name": "Jerin"
            }
        ],
        "name": "Developer"
    },
    {
        "id": 2,
        "members": [
            {
                "id": 1,
                "name": "Jerin"
            }
        ],
        "name": "Team Lead"
    }
]

Here, the members field returning the Person information, which is perfect.

But,

How can I add the date_joined and invite_reason field/info into the members field of the JSON response?

JPG
  • 82,442
  • 19
  • 127
  • 206

1 Answers1

1
class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = "__all__"

    def serialize_membership(self, person_instance):
        # simple method to serialize the through model fields
        membership_instance = person_instance \
            .membership_set \
            .filter(group=self.context["group_instance"]) \
            .first()

        if membership_instance:
            return MembershipSerializer(membership_instance).data
        return {}

    def to_representation(self, instance):
        rep = super().to_representation(instance)
        return {**rep, **self.serialize_membership(instance)}


class MembershipSerializer(serializers.ModelSerializer): # create new serializer to serialize the through model fields
    class Meta:
        model = Membership
        fields = ("date_joined", "invite_reason")


class GroupSerializer(serializers.ModelSerializer):
    members = serializers.SerializerMethodField() # use `SerializerMethodField`, can be used to pass context data 

    def get_members(self, group):
        return PersonSerializer(
            group.members.all(),
            many=True,
            context={"group_instance": group}  # should pass this `group` instance as context variable for filtering
        ).data

    class Meta:
        model = Group
        fields = "__all__"

Notes:

  • Override the to_representation(...) method of PersonSerializer to inject extra data into the members field of the JSON
  • We need person instance/pk and group instance/pk to identify the Membership instance to be serialized. For that, we have used the serializer context to pass essential data
JPG
  • 82,442
  • 19
  • 127
  • 206
  • Thanks for the very detailed answer but I cannot get my implementation to recognize that the appropriate keyword exists in `self.context`. I am trying to adapt this code to my implementation. It fails on this line: `.filter(group=self.context["group_instance"]) \` (but using my terminology. I have updated my original question which prompted this Q&A to show how I tried to adapt this code and maybe you can see where I went wrong, but I've tried to go over it several times and have not found a significant difference. – wfgeo Dec 30 '20 at 12:17
  • The reason it wasn't working was because I had the conceptual mapping backwards. What should have been my `Group` objects were actually my `Person` objects and vice versa. Reversing the mapping fixed it. – wfgeo Dec 30 '20 at 12:57
  • This worked for me. Doing things with `ReadOnlyField(source="...")` to include fields from the relation,... as I could read in other answers didn't. Thank for this answer. – lbris Jun 01 '23 at 10:03
  • in my case, the code also fails on `.filter(group=self.context["group_instance"]) `. Models and serializers are the same as JPG's; perhaps the issue is with how my views are defined (the context is not passed to the serializer correctly)? ``` from rest_framework import viewsets class PersonViewSet(viewsets.ModelViewSet): queryset = Person.objects.all() serializer_class = PersonSerializer ``` – peterv Jul 11 '23 at 14:57