Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Need help understanding many and source fields in a serializer

I am currently trying to familiarize myself with DRF and while going through a tutorial these serializers were used

class EmbeddedAnswerSerializer(serializers.ModelSerializer):
    votes = serializers.IntegerField(read_only=True)

    class Meta:
        model = Answer
        fields = ('id', 'text', 'votes',)


class QuestionSerializer(serializers.ModelSerializer):
    answers = EmbeddedAnswerSerializer(many=True,source='answer_set')
    class Meta:
        model = Question
        fields = ('id', 'answers', 'created_at', 'text', 'user_id',)

These are the models

class Question(models.Model):
    user_id = models.CharField(max_length=36)
    text = models.CharField(max_length=140)
    created_at = models.DateTimeField(auto_now_add=True)


class Answer(models.Model):
    question = models.ForeignKey(Question,on_delete=models.PROTECT)
    text = models.CharField(max_length=25)
    votes = models.IntegerField(default=0)

My question is in the statement in the Question serializer

answers = EmbeddedAnswerSerializer(many=True,source='answer_set')

what is the purpose of many = True and source='answer_set' ? I read from the documentation the following regarding many=True

You can also still use the many=True argument to serializer classes. It's worth noting that many=True argument transparently creates a ListSerializer instance, allowing the validation logic for list and non-list data to be cleanly separated in the REST framework codebase.

I am confused by what that means? If I remove many=True from the code I get the error

AttributeError at /api/quest/1/2/
Got AttributeError when attempting to get a value for field `text` on serializer `EmbeddedAnswerSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `RelatedManager` instance.
Original exception text was: 'RelatedManager' object has no attribute 'text'.

Can anyone explain what many=True does and what source field does?

like image 292
MistyD Avatar asked Dec 17 '22 23:12

MistyD


2 Answers

Adding to the answer above by @neverwalkaloner

Many = True

many=True signals that there is more than one object (an iterable) being passed to the serializer. Passing this field in turn triggers the many_init within BaseSerializer to automagically create a ListSerializer instance.

Source Code Snippet:

def __new__(cls, *args, **kwargs):
    # We override this method in order to automagically create
    # `ListSerializer` classes instead when `many=True` is set.
    if kwargs.pop('many', False):
        return cls.many_init(*args, **kwargs)
    return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)

Source = "xyz"

This tells DRF which object attribute supplies the value for the field. The default assumption is that the field name declared on the serializer is the same as the field on the object instance that supplies the value. In cases where this is not true, source allows you to explicitly supply the object instance where the serializer will look for the value. Here's a peek into the def bind(self, field_name, parent) inside serializers.fields where this happens

Source Code Snippet:

    # self.source should default to being the same as the field name.
    if self.source is None:
        self.source = field_name

    # self.source_attrs is a list of attributes that need to be looked up
    # when serializing the instance, or populating the validated data.
    if self.source == '*':
        self.source_attrs = []
    else:
        self.source_attrs = self.source.split('.')

Finally the value is gotten as follows using the source and source_attrs declared in bind:

def get_attribute(self, instance):
    """
    Given the *outgoing* object instance, return the primitive value
    that should be used for this field.
    """
    try:
        return get_attribute(instance, self.source_attrs)
    except (KeyError, AttributeError) as exc:

Assuming a Question can have multiple Answers, your approach is correct.

The problem appears to be that the source you supplied is the RelatedManager instance itself, and not the queryset of Answer objects. I assumed DRF resolves this accurately, can you try changing it to source='answer_set.all'.

answer_set is the default RelatedManager name given by Django. It might be wise to name your backward relationship using related_name in the Django model. This can be achieved by changing:

question = models.ForeignKey(Question,on_delete=models.PROTECT, related_name='answers')
like image 185
rtindru Avatar answered Dec 20 '22 18:12

rtindru


Probably not the best explanation and someone can add more details but briefly many=True tells to serializer that it will take list of objects for serilizing proccess. In other words it's just a trigger that allows you to specify will you serialize, well, many objects at once, or just single object.

source on the other side specify which attribute of objects should be serializing with current serializer's field.

In practice this line

answers = EmbeddedAnswerSerializer(many=True, source='answer_set')

means that you want to serialize answer_set attribute of Question object with EmbeddedAnswerSerializer. Since answer_set is list of object you should add many=True as argument to make serializer aware that it will work with list of objects instead of single object.

like image 20
neverwalkaloner Avatar answered Dec 20 '22 17:12

neverwalkaloner