Search code examples
jsondjangodjango-rest-frameworkmany-to-many

How to modify a many-to-many collection using django rest framework


I am trying to create an endpoint where, having a User entity, I can add / remove existing Group entities to user.groups many-to-many field. But when I try to do it, django-rest-framework tries to create new group objects instead of finding existing ones.

I have defined two serializers where UserSerializer has a nested GroupSerializer:

class GroupSerializer(serializers.ModelSerializer):
    class Meta:
        model = Group
        fields = ['id', 'name']


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'groups']

    groups = GroupSerializer(many=True)

    def update(self, instance, validated_data):
        data = validated_data.copy()
        groups = data.pop('groups', [])
        for key, val in data.items():
            setattr(instance, key, val)
        instance.groups.clear()
        for group in groups:
            instance.groups.add(group)
        return instance

    def create(self, validated_data):
        data = validated_data.copy()
        groups = data.pop('groups', [])
        instance = self.Meta.model.objects.create(**data)
        for group in groups:
            instance.groups.add(group)
        return instance

When I send a JSON through a PUT REST call (from django-rest-framework web interface):

{
    "id": 6,
    "username": "[email protected]",
    "email": "[email protected]",
    "groups": [
        {
            "id": 1,
            "name": "AAA"
        }
    ]
}

I expect serializer to find the Group with given id and add it to User. But instead, it tries to create a new user group and fails with duplicate key error:

{
    "groups": [
        {
            "name": [
                "group with this name already exists."
            ]
        }
    ]
}

I searched over the internet and debugged myself and found no solution to this use case.

The create and update methods inside UserSerializerclass are never reached.

Edit: as asked, here are my views and urls:

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer


class GroupViewSet(viewsets.ModelViewSet):
    queryset = Group.objects.all()
    serializer_class = GroupSerializer

Urls:

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)


urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

Solution

  • This seems to be the validation error due to a nested serializer model that contains unique constraint, see this post. According to the article, DRF did not handle this condition since it's hard to realize if the serializer is a nested serializer within another one. And that's why the create() and update() never been reached since the validation is done before calling them.

    The way to work around this is to remove the uniqueness validator manually in GroupSerializer as follow:

    class GroupSerializer(serializers.ModelSerializer):
        class Meta:
            model = Group
            fields = ['id', 'name']
            extra_kwargs = {
                'name': {'validators': []},
            }
    

    BTW, there are some points that can be improved or should be corrected in your update() and create() code. Firstly, you didn't do instance.save() so the instance won't be update after the whole process done. Second, the groups are just a list of dictionary, and you should not add object that way. The following are the modification based on your OP code:

        def update(self, instance, validated_data):
            data = validated_data.copy()
            groups = data.pop('groups', [])
            for key, val in data.items():
                setattr(instance, key, val)
            instance.save()                     # This will indeed update DB values
    
            group_ids = [g['id'] for g in groups]
            instance.groups.clear()
            instance.groups.add(*group_ids)     # Add all groups once. Also you can replace these two lines with
                                                # instance.groups.set(group_ids)
            return instance