Search code examples
django-rest-frameworkdjango-serializer

Django REST: Save Many-to-Many Association with many=True


SCENARIO

I have a many-to-many relationship between two models:

Supplier

class Supplier(models.Model):
  class Meta:
    unique_together = ['supplier_no', 'supplier_name']
    ordering = ['supplier_name']

    supplier_no = models.IntegerField(blank=False, null=False)
    supplier_name = models.CharField(max_length=180)
    ...
    updated = models.DateTimeField(auto_now=True, blank=True)
    updated_by = models.ForeignKey(UsaUser, on_delete=models.CASCADE, blank=True, null=True,
                               related_name='supplierUpdatedByUser')

    def __str__(self):
      return self.supplier_name

Plant

class Plant(models.Model):
  class Meta:
    unique_together = ['plant_no', 'plant_name']
    ordering = ['plant_no']

    plant_no = models.IntegerField(unique=True)
    plant_name = models.CharField(max_length=180, unique=True)
    ...
    suppliers = models.ManyToManyField(Supplier, related_name='plants')
    updated = models.DateTimeField(auto_now=True, blank=True)
    updated_by = models.ForeignKey(UsaUser, on_delete=models.CASCADE, blank=True, null=True,
                               related_name='plantUpdatedByUser')

    def __str__(self):
        return self.plant_name

Simply put: a supplier can be active in any number of plants, and a plant can have any number of suppliers.

And here is the SupplierSerializer with the create method in question:

class SupplierSerializer(serializers.ModelSerializer):
    plants = PlantSerializer(many=True)  # serializes entire supplier objects instead of just returning the pk

    class Meta:
        model = Supplier
        fields = ['id', 'supplier_no', 'supplier_name', ... 'plants']

    def create(self, validated_data):
        # Add the UsaUser as a blameable field, which will be passed in the 'context' object to the serializer.
        validated_data.update({"updated_by": self.context['request'].user})
        assoc_plants = validated_data.pop('plants')  # remove the many-to-many association from the data before saving
        supplier = Supplier.objects.create(**validated_data)
        # Now add in the associated plants
        for plant in assoc_plants:
            supplier.plants.add(plant)
        return supplier

...

PROBLEM:

When creating a Supplier, I get the following 400 response:

{"plants":[{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]},{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}]

If I remove the following line from my serializer:

plants = PlantSerializer(many=True)

the problem is resolved. However, I want full plant objects to be returned to my front end, not just the plant ids.

I thought maybe I needed to return the full Plant object since the error says it's looking for a dictionary, but then I get another error:

{"plants":[{"plant_no":["plant with this plant no already exists."],"plant_name":["plant with this plant name already exists."]}]}

Sample Request Payload

{"supplier_no":"5052","supplier_name":"MySupplier","is_active":true,"plants":[1,10]}

^^ 1 and 10 are the pks of the Plants

When passing a whole Plant object:

{"supplier_no":"54564","supplier_name":"MySuppleir","is_active":true,"plants":[{"id":1,"plant_no":1,"plant_name":"AutoPlant1","is_active":true...}]}


Solution

  • I think you want to use the plants that are already created. Then you can use some extra fields.

    class SupplierSerializer(serializers.ModelSerializer):
        plants = PlantSerializer(many=True, read_only = True)  # set it as read_only
        plant_ids = serializers.ListField(
            child = serializers.IntegerField(),
            write_only = True
        )
    
        class Meta:
            model = Supplier
            fields = [..., 'plant_ids'] # add plant_ids
    
        def create(self, validated_data):        
            plant_ids = validated_data.pop('plant_ids')
            supplier = Supplier.objects.create(**validated_data)
            supplier.plants.set(plant_ids)
    
            return supplier