Search code examples
djangodjango-modelsdjango-rest-frameworkdjango-serializerdjango-database

DRF 2-way nested serialization with many to many relationship


I have a many-to-many relationship between my Student and Macro models, using an intermediary model

class Person(models.Model):
    # cf/NIN optional by design
    cf = models.CharField(_('NIN'), unique=True, blank=True, null=True, max_length=16)
    first_name = models.CharField(_('first name'), blank=False, max_length=40)
    last_name = models.CharField(_('last name'), blank=False, max_length=40)
    date_of_birth = models.DateField(_('date of birth'), blank=False)

    class Meta:
        ordering = ['last_name', 'first_name']
        abstract = True

    def __str__(self):
        return self.first_name + ' ' + self.last_name


class Macro(models.Model):
    name = models.CharField(_('name'), unique=True, blank=False, max_length=100)
    description = models.TextField(_('description'), blank=True, null=True)

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name


class Student(Person):
    enrollment_date = models.DateField(_('enrollment date'), blank=True, null=True)
    description = models.TextField(_('description'), blank=True, null=True)
    macro = models.ManyToManyField(Macro, through='MacroAssignement')


class MacroAssignement(models.Model):
    student = models.ForeignKey(Student, related_name='macros', on_delete=models.CASCADE)
    macro = models.ForeignKey(Macro, related_name='students', on_delete=models.CASCADE)

    def __str__(self):
        return str(self.student)

I configure serializers in order to exploit the nested serialization when I serialize students

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ('id',
                  'cf',
                  'first_name',
                  'last_name',
                  'date_of_birth')
        abstract = True


class StudentSerializer(PersonSerializer):
    macro = serializers.StringRelatedField(many=True, read_only=True)

    class Meta(PersonSerializer.Meta):
        model = Student
        fields = PersonSerializer.Meta.fields + ('enrollment_date',
                                                 'description',
                                                 'macro')
        extra_kwargs = {'enrollment_date': {'default': date.today()},
                        'description': {'required': False}}


class MacroSerializer(serializers.ModelSerializer):
    students = StudentSerializer(many=True, read_only=True)

    class Meta:
        model = Macro
        fields = ('id',
                  'name',
                  'description',
                  'students')

Untill here no problem, when I request student data, the macro related information comes along with it. Here's an example

    {
        "id": 18,
        "cf": "ciaciacia",
        "first_name": "Paolo",
        "last_name": "Bianchi",
        "date_of_birth": "2020-05-01",
        "enrollment_date": null,
        "description": null,
        "macro": [
            "macro1"
        ]
    },

Now, on the contrary, when I request for a macro, I would like to view also the related students list. I've tried to implement nested serialization also in the MacroSerializer

class MacroSerializer(serializers.ModelSerializer):
        students = StudentSerializer(many=True, read_only=True)

This doesn't work, as I get the following error

AttributeError: Got AttributeError when attempting to get a value for field `first_name` on serializer `StudentSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `MacroAssignement` instance.
Original exception text was: 'MacroAssignement' object has no attribute 'first_name'.

[NOTE: first_name is a field of Student model inherited from Person model]

Of course I could implement a function to query the database and get the name of students assigned to a given macro, but I'm wondering if there's a buil-in django way of doing it. Kind of like 2-way nested serialization


Solution

  • As stated in previous helpful comments, the fix is about using related_name. My code has been modified as following

    serializers.py

    class StudentSerializer(PersonSerializer):
        macro = serializers.StringRelatedField(many=True, read_only=True)
    
        class Meta(PersonSerializer.Meta):
            model = Student
            fields = PersonSerializer.Meta.fields + ('enrollment_date',
                                                     'description',
                                                     'macro')
            extra_kwargs = {'enrollment_date': {'default': date.today()},
                            'description': {'required': False}}
    
    class MacroSerializer(serializers.ModelSerializer):
        students = StudentSerializer(many=True, read_only=True)
    
        class Meta:
            model = Macro
            fields = ('id',
                      'name',
                      'description',
                      'students')
    

    models.py

    class Student(Person):
        enrollment_date = models.DateField(_('enrollment date'), blank=True, null=True)
        description = models.TextField(_('description'), blank=True, null=True)
        macro = models.ManyToManyField(Macro, through='MacroAssignement', related_name='students')
    

    Note that what I wrote in my question is actually a poor design decision, because it would lead to a circular dependency. Indeed, StudentSerializer would need MacroSerializer and viceversa. I highly suggest to read this question about dependencies in serializers

    ************ EDITED ************

    Quick fix: set depth = 1 in the related model serializer (in this case, StudentSerializer) Thanks to @neverwalkaloner answer on this question