How to properly update a many to many nested serializer?

Question:

I have been able to replicate the create method to add the correct nested serializers in a POST request. However, I’m still having issues updating in a PUT or PATCH. When using a PUT or PATCH request and I pass the entire object data or the "brands" data, it will only update in the position it is passed. So if I have an object with 3 values:

"brands": [
            {
                "id": 1,
                "name": "Brand 1 Test"
            },
            {
                "id": 2,
                "name": "Brand 2 Test"
            },
            {
                "id": 3,
                "name": "Brand 3 Test"
            }
}

If I pass:

"brands": [
            {
                "id": 1,
                "name": "Brand 1 Test"
            },
            {
                "id": 2,
                "name": "Brand 2 Test"
            }

It will give me the same list of 3 brands. But if I do that in reverse order it will update and add the 3rd brand. I’m not sure what’s causing it. Here’s the code I have:

Models

class Brand(models.Model):
    name = models.CharField(max_length=500)

class Incentive(models.Model):
    name = models.CharField(max_length=500)
    brands = models.ManyToManyField(Brand, related_name='incentives_brand')
    start_dt = models.DateTimeField(auto_now_add=False, blank=True, null=True)
    end_dt = models.DateTimeField(auto_now_add=False, blank=True, null=True)

Serializers

class BrandSerializer(serializers.ModelSerializer):
    class Meta:
        model = Brand
        depth = 1
        fields = ['id', 'name']

class IncentiveSerializer(serializers.ModelSerializer):
    brands = BrandSerializer(many=True)
    
    class Meta:
        model = Incentive
        fields = ['id', 'name', 'brands', 'start_dt', 'end_dt']
    
    def create(self, validated_data):
        brands = validated_data.pop('brands', [])
        instance = Incentive.objects.create(**validated_data)
        for brand_data in brands:
            brand = Brand.objects.get(**brand_data)
            instance.brands.add(brand)
        return instance 

    def update(self, instance, validated_data):
        brands = validated_data.pop('brands', [])
        instance = super().update(instance, validated_data)
        for brand_data in brands:
            brand = Brand.objects.get(**brand_data)
            instance.brands.add(brand)
        return instance

I think the issue lies somewhere here. If any more code is needed please let me know(ex. views, urls). I’m guessing in the update I’m not properly emptying the list of brands. I just can’t see it. Any help would be appreciated.

Asked By: Kllicks

||

Answers:

I think the clue here is that you do instance.brands.add, which does exactly that, adding. Not removing as you noticed 🙂

You also have a set.

So:

brand_objs = []
for brand_data in brands:
    brand = Brand.objects.get(**brand_data)
    brand_objs.append(brand)

instance.brands.set(brand_objs)

But the usage could differ, I can imagine that you’d also want to be able to just add one, or more, brands? But could use different end points for that?

Endpoints example

api/incentive/1/brands # get
api/incentive/1/brands # post, set brands?
api/incentive/1/brands/add # add one or more?
api/incentive/1/brands/remove # remove specific one or more?
Answered By: The Pjot

Add instance.brands.clear() like so:

This will clear related brands so you can update them freshly.

def update(self, instance, validated_data):
    brands = validated_data.pop('brands', None)
    instance = super().update(instance, validated_data)
    # The condition below will update brands only if brands were 
    # specified in the request body
    if brands is not None:
        instance.brands.clear() # Clear related brands
        for brand_data in brands:
            brand = Brand.objects.get(**brand_data)
            instance.brands.add(brand)
        return instance
Answered By: Solomon Botchway