How to handle pre-selection of related fields and ensure proper update of many-to-many relationships in Django REST Framework?

I am working on implementing a Role-Based Access Control using Django and Django Rest Framework. I want to create a role with a set of permissions through the DRF browsable API. Additionally, I need the functionality to update those permissions, including adding new ones and removing existing ones. When displaying a role in the browsable API, I want the associated permissions to be pre-selected for clarity, while also showing all other available permissions for easy addition.

What I have done so far

Here is a simplified version of my model

class BaseModel(models.Model):
    pkid = models.BigAutoField(primary_key=True, editable=False)
    id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        abstract = True

class Role(BaseModel):
    name = models.CharField(max_length=100)

class Permission(BaseModel):
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(null=True, blank=True)

class RolePermission(BaseModel):
    role = models.ForeignKey(
        Role, on_delete=models.CASCADE, related_name="role_permissions"
    )
    permission = models.ForeignKey(Permission, on_delete=models.CASCADE)

Here is my serializer

class RoleSerializer(serializers.ModelSerializer):
    permissions = serializers.SlugRelatedField(
        queryset=Permission.objects.all(), many=True, required=False, slug_field="name"
    )
    business = serializers.PrimaryKeyRelatedField(
        queryset=Business.objects.all(), required=False, allow_null=True
    )

    class Meta:
        model = Role
        fields = ("id", "name", "is_default", "permissions", "business")

    def to_representation(self, instance):
        representation = super().to_representation(instance)
        permissions = instance.role_permissions.all().values_list(
            "permission__name", flat=True
        )
        representation["permissions"] = list(permissions)
        return representation

    def to_internal_value(self, data):
        data = data.copy()
        permissions_data = data.pop("permissions", [])
        permissions_qs = Permission.objects.filter(name__in=permissions_data)
        data["permissions"] = [permission.id for permission in permissions_qs]
        return super().to_internal_value(data)

The preselection works and all but when I send an UPDATE request to modify the permissions, I am getting errors like:

{
    "permissions": [
        "Object with name=[UUID('e4ac49c0-093a-4281-96b0-0846917fc62b')] does not exist."
    ]
}

I fixed the error by rewriting my serializer to something like this

class RoleSerializer(serializers.ModelSerializer):
    permissions = serializers.PrimaryKeyRelatedField(
        queryset=Permission.objects.all(), many=True, required=False
    )
    business = serializers.PrimaryKeyRelatedField(
        queryset=Business.objects.all(), required=False, allow_null=True
    )

    class Meta:
        model = Role
        fields = ("id", "name", "is_default", "permissions", "business")

    def to_representation(self, instance):
        representation = super().to_representation(instance)
        permissions = instance.role_permissions.all().values_list(
            "permission", flat=True
        )
        representation["permissions"] = list(permissions)
        return representation

    def to_internal_value(self, data):
        data = data.copy()
        permissions_data = data.pop("permissions", [])
        internal_value = super().to_internal_value(data)
        internal_value["permissions"] = permissions_data
        return internal_value

but doing this affects the json returned, now instead of the permission name I get the IDs like this

{
    "id": "0b3de100-82ef-4a02-b94e-659cb6dd4314",
    "name": "neweee",
    "is_default": false,
    "business": null,
    "permissions": [
        1,
        2
    ]
}

My question: How can I correctly pre-select related fields (permissions) as names in the DRF browsable API, while still being able to create/update with no errors?

Thank you

PrimaryKeyRelatedField may represent the target of the relationship using its primary key, as the documentation said. here

SlugRelatedField may be used to represent the target of the relationship using a field on the target. here

For example:

class AlbumSerializer(serializers.ModelSerializer):
    tracks = serializers.SlugRelatedField(
        many=True,
        read_only=True,
        slug_field='title'
     )

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']

Output:

{
    'album_name': 'Dear John',
    'artist': 'Loney Dear',
    'tracks': [
        'Airport Surroundings',
        'Everything Turns to You',
        'I Was Only Going Out',
        ...
    ]
}

Personal solution:

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

use this serializer in your RoleSerializer

class RoleSerializer(serializers.ModelSerializer):
    permissions = PermissionSerializer(many=True, required=False)
    # remaining code here .....
Back to Top