Django Rest Framework: Disable field update after object is created

DjangoRestDjango Rest-Framework

Django Problem Overview


I'm trying to make my User model RESTful via Django Rest Framework API calls, so that I can create users as well as update their profiles.

However, as I go through a particular verification process with my users, I do not want the users to have the ability to update the username after their account is created. I attempted to use read_only_fields, but that seemed to disable that field in POST operations, so I was unable to specify a username when creating the user object.

How can I go about implementing this? Relevant code for the API as it exists now is below.

class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'password', 'email')
        write_only_fields = ('password',)

    def restore_object(self, attrs, instance=None):
        user = super(UserSerializer, self).restore_object(attrs, instance)
        user.set_password(attrs['password'])
        return user


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    serializer_class = UserSerializer
    model = User

    def get_permissions(self):
        if self.request.method == 'DELETE':
            return [IsAdminUser()]
        elif self.request.method == 'POST':
            return [AllowAny()]
        else:
            return [IsStaffOrTargetUser()]

Thanks!

Django Solutions


Solution 1 - Django

It seems that you need different serializers for POST and PUT methods. In the serializer for PUT method you are able to just except the username field (or set the username field as read only).

class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    serializer_class = UserSerializer
    model = User

    def get_serializer_class(self):
        serializer_class = self.serializer_class
        
        if self.request.method == 'PUT':
            serializer_class = SerializerWithoutUsernameField

        return serializer_class

    def get_permissions(self):
        if self.request.method == 'DELETE':
            return [IsAdminUser()]
        elif self.request.method == 'POST':
            return [AllowAny()]
        else:
            return [IsStaffOrTargetUser()]

Check this question https://stackoverflow.com/questions/22104487/django-rest-framework-independent-get-and-put-in-same-url-but-different-generic/22107326#22107326

Solution 2 - Django

Another option (DRF3 only)

class MySerializer(serializers.ModelSerializer):
    ...
    def get_extra_kwargs(self):
        extra_kwargs = super(MySerializer, self).get_extra_kwargs()
        action = self.context['view'].action

        if action in ['create']:
            kwargs = extra_kwargs.get('ro_oncreate_field', {})
            kwargs['read_only'] = True
            extra_kwargs['ro_oncreate_field'] = kwargs

        elif action in ['update', 'partial_update']:
            kwargs = extra_kwargs.get('ro_onupdate_field', {})
            kwargs['read_only'] = True
            extra_kwargs['ro_onupdate_field'] = kwargs

        return extra_kwargs

Solution 3 - Django

Another method would be to add a validation method, but throw a validation error if the instance already exists and the value has changed:

def validate_foo(self, value):                                     
    if self.instance and value != self.instance.foo:
        raise serializers.ValidationError("foo is immutable once set.")
    return value         

In my case, I wanted a foreign key to never be updated:

def validate_foo_id(self, value):                                     
    if self.instance and value.id != self.instance.foo_id:            
        raise serializers.ValidationError("foo_id is immutable once set.")
    return value         

See also: https://stackoverflow.com/questions/31089407/level-field-validation-in-django-rest-framework-3-1-access-to-the-old-value

Solution 4 - Django

My approach is to modify the perform_update method when using generics view classes. I remove the field when update is performed.

class UpdateView(generics.UpdateAPIView):
    ...
    def perform_update(self, serializer):
        #remove some field
        rem_field = serializer.validated_data.pop('some_field', None)
        serializer.save()

Solution 5 - Django

I used this approach:

def get_serializer_class(self):
    if getattr(self, 'object', None) is None:
        return super(UserViewSet, self).get_serializer_class()
    else:
        return SerializerWithoutUsernameField

Solution 6 - Django

UPDATE:

Turns out Rest Framework already comes equipped with this functionality. The correct way of having a "create-only" field is by using the CreateOnlyDefault() option.

I guess the only thing left to say is Read the Docs!!! http://www.django-rest-framework.org/api-guide/validators/#createonlydefault

Old Answer:

Looks I'm quite late to the party but here are my two cents anyway.

To me it doesn't make sense to have two different serializers just because you want to prevent a field from being updated. I had this exact same issue and the approach I used was to implement my own validate method in the Serializer class. In my case, the field I don't want updated is called owner. Here is the relevant code:

class BusinessSerializer(serializers.ModelSerializer):

    class Meta:
	    model = Business
	    pass

    def validate(self, data):
	    instance = self.instance

	    # this means it's an update
        # see also: http://www.django-rest-framework.org/api-guide/serializers/#accessing-the-initial-data-and-instance
	    if instance is not None: 
		    originalOwner = instance.owner

		    # if 'dataOwner' is not None it means they're trying to update the owner field
		    dataOwner = data.get('owner') 
		    if dataOwner is not None and (originalOwner != dataOwner):
			    raise ValidationError('Cannot update owner')
	    return data
    pass
pass

And here is a unit test to validate it:

def test_owner_cant_be_updated(self):
	harry = User.objects.get(username='harry')
	jack = User.objects.get(username='jack')

	# create object
	serializer = BusinessSerializer(data={'name': 'My Company', 'owner': harry.id})
	self.assertTrue(serializer.is_valid())
	serializer.save()
	
	# retrieve object
	business = Business.objects.get(name='My Company')
	self.assertIsNotNone(business)

	# update object
	serializer = BusinessSerializer(business, data={'owner': jack.id}, partial=True)

    # this will be False! owners cannot be updated!
	self.assertFalse(serializer.is_valid())
	pass

I raise a ValidationError because I don't want to hide the fact that someone tried to perform an invalid operation. If you don't want to do this and you want to allow the operation to be completed without updating the field instead, do the following:

remove the line:

raise ValidationError('Cannot update owner')

and replace it with:

data.update({'owner': originalOwner})

Hope this helps!

Solution 7 - Django

More universal way to "Disable field update after object is created"

  • adjust read_only_fields per View.action
  1. add method to Serializer (better to use your own base cls)

    def get_extra_kwargs(self): extra_kwargs = super(BasePerTeamSerializer, self).get_extra_kwargs() action = self.context['view'].action actions_readonly_fields = getattr(self.Meta, 'actions_readonly_fields', None) if actions_readonly_fields: for actions, fields in actions_readonly_fields.items(): if action in actions: for field in fields: if extra_kwargs.get(field): extra_kwargs[field]['read_only'] = True else: extra_kwargs[field] = {'read_only': True} return extra_kwargs

  2. Add to Meta of serializer dict named actions_readonly_fields

    class Meta: model = YourModel fields = 'all' actions_readonly_fields = { ('update', 'partial_update'): ('client', ) }

In the example above client field will become read-only for actions: 'update', 'partial_update' (ie for PUT, PATCH methods)

Solution 8 - Django

This post mentions four different ways to achieve this goal.

This was the cleanest way I think: [collection must not be edited]

class DocumentSerializer(serializers.ModelSerializer):

    def update(self, instance, validated_data):
        if 'collection' in validated_data:
            raise serializers.ValidationError({
                'collection': 'You must not change this field.',
            })

        return super().update(instance, validated_data)

Solution 9 - Django

Another solution (apart from creating a separate serializer) would be to pop the username from attrs in the restore_object method if the instance is set (which means it's a PATCH / PUT method):

def restore_object(self, attrs, instance=None):
    if instance is not None:
        attrs.pop('username', None)
    user = super(UserSerializer, self).restore_object(attrs, instance)
    user.set_password(attrs['password'])
    return user

Solution 10 - Django

If you don't want to create another serializer, you may want to try customizing get_serializer_class() inside MyViewSet. This has been useful to me for simple projects.

# Your clean serializer
class MySerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = '__all__'

# Your hardworking viewset
class MyViewSet(MyParentViewSet):
    serializer_class = MySerializer
    model = MyModel

    def get_serializer_class(self):
        serializer_class = self.serializer_class
        if self.request.method in ['PUT', 'PATCH']:
            # setting `exclude` while having `fields` raises an error
            # so set `read_only_fields` if request is PUT/PATCH
            setattr(serializer_class.Meta, 'read_only_fields', ('non_updatable_field',))
            # set serializer_class here instead if you have another serializer for finer control
        return serializer_class

> # setattr(object, name, value) > This is the counterpart of getattr(). The > arguments are an object, a string and an arbitrary value. The string > may name an existing attribute or a new attribute. The function > assigns the value to the attribute, provided the object allows it. For > example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.

Solution 11 - Django

class UserUpdateSerializer(UserSerializer):
    class Meta(UserSerializer.Meta):
        fields = ('username', 'email')

class UserViewSet(viewsets.ModelViewSet):
    def get_serializer_class(self):
        return UserUpdateSerializer if self.action == 'update' else super().get_serializer_class()

djangorestframework==3.8.2

Solution 12 - Django

I would suggest also looking at Django pgtrigger

This allows you to install triggers for validation. I started using it and was very pleased with its simplicity:

Here's one of their examples that prevents a published post from being updated:

import pgtrigger
from django.db import models


@pgtrigger.register(
    pgtrigger.Protect(
        operation=pgtrigger.Update,
        condition=pgtrigger.Q(old__status='published')
    )
)
class Post(models.Model):
    status = models.CharField(default='unpublished')
    content = models.TextField()

The advantage of this approach is it also protects you from .update() calls that bypass .save()

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBrad ReardonView Question on Stackoverflow
Solution 1 - DjangoAndrei KaigorodovView Answer on Stackoverflow
Solution 2 - DjangoVoSiView Answer on Stackoverflow
Solution 3 - DjangorrauenzaView Answer on Stackoverflow
Solution 4 - DjangoGooshanView Answer on Stackoverflow
Solution 5 - DjangoAlex RothbergView Answer on Stackoverflow
Solution 6 - DjangoLuisCienView Answer on Stackoverflow
Solution 7 - DjangopymenView Answer on Stackoverflow
Solution 8 - DjangoHojat ModaresiView Answer on Stackoverflow
Solution 9 - DjangoPawel KozelaView Answer on Stackoverflow
Solution 10 - DjangoNogurennView Answer on Stackoverflow
Solution 11 - DjangogzeroneView Answer on Stackoverflow
Solution 12 - DjangorrauenzaView Answer on Stackoverflow