Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serializing custom related field in DRF

I am trying to make a serializer with a nested "many to many" relationship. The goal is to get a serialized JSON object contain an array of serialized related objects. The models look like this (names changed, structure preserved)

from django.contrib.auth.models import User

PizzaTopping(models.Model):
    name = models.CharField(max_length=255)
    inventor = models.ForeignKey(User)

Pizza(models.Model):
    name = models.CharField(max_length=255)
    toppings = models.ManyToManyField(PizzaTopping)

The incoming JSON looks like this

{
  "name": "My Pizza",
  "toppings": [
    {"name": "cheese", "inventor": "bob"},
    {"name": "tomatoes", "inventor": "alice"}
  ]
}

My current serializer code looks like this

class ToppingRelatedField(RelatedField):
    def get_queryset(self):
        return Topping.objects.all()

    def to_representation(self, instance):
        return {'name': instance.name, 'inventor': instance.inventor.username}

    def to_internal_value(self, data):
        name = data.get('name', None)
        inventor = data.get('inventor', None)
        try:
            user = User.objects.get(username=inventor)
        except Setting.DoesNotExist:
            raise serializers.ValidationError('bad inventor')
        return Topping(name=name, inventor=user)

class PizzaSerializer(ModelSerializer):
    toppings = ToppingRelatedField(many=True)

    class Meta:
        model = Pizza
        fields = ('name', 'toppings')

It seems that since I defined the to_internal_value() for the custom field, it should create/update the many-to-many field automatically. But when I try to create pizzas, I get "Cannot add "": the value for field "pizzatopping" is None" ValueError. It looks like somewhere deep inside, Django decided that the many to many field should be called by the model name. How do I convince it otherwise?

Edit #1: It seems that this might be a genuine bug somewhere in Django or DRF. DRF seems to be doing the right thing, it detects that it is dealing with a ManyToMany field and tries to create toppings from the data using the custom field and add them to the pizza. Since it only has a pizza instance and a field name, it uses setattr(pizza, 'toppings', toppings) to do it. Django seems to be doing the right thing. The __set__ is defined and seems to figure out that it needs to use add() method in the manager. But somewhere along the way, the field name 'toppings' gets lost and replaced by the default. Which is "related model name in lower case".

Edit #2: I have found a solution. I will document it in an answer once I am allowed. It seems that the to_internal_value() method in the RelatedField subclass needs to return a saved instance of a Topping for the ManyToMany thing to work properly. The existing docs show the opposite, a this link (http://www.django-rest-framework.org/api-guide/fields/#custom-fields) the example clearly returns an unsaved instance.

like image 244
Mad Wombat Avatar asked Sep 26 '16 15:09

Mad Wombat


1 Answers

Seems like there is an undocumented requirement. For write operations to work with a custom ManyToMany field, the custom field class to_internal_value() method needs to save the instance before returning it. The DRF docs omit this and the example of making a custom field (at http://www.django-rest-framework.org/api-guide/fields/#custom-fields) shows the method returning an unsaved instance. I am going to update the issue I opened with the DRF team.

like image 186
Mad Wombat Avatar answered Nov 17 '22 00:11

Mad Wombat