Search code examples
pythondjangodjango-rest-frameworkone-to-one

Updating related objects django rest framework


In my django project, I have 2 relevant models:

class User(AbstractUser):
    email = models.EmailField(
        verbose_name='e-mailaddress',
        max_length=255,
        unique=True)
    # other props not important
        
class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    #other props not important

For both creating and retrieving, it is convenient to have User as a nested object in the serializer:

class ProfileSerializer(serializers.ModelSerializer):
    user = UserSerializer()
    class Meta:
        model = Profile
        fields = '__all__'

    def create(self, validated_data):
        user_data = validated_data.pop('user')
        user = User.objects.create(**user_data)
        user.save()
        # because of reasons, an empty profile is created on initial save. This works, trust me
        d, _ = Profile.objects.update_or_create(user=user, defaults=validated_data)
        d.save()
        return d

HOWEVER. One action that I want to be possible as well is to update both properties of the Profile and the User at the same time. When doing this with my serializer, serializer.is_valid() fails as the provided email is already present in the database. This also indicates that, even if the validation passes (i.e. because of an updated email address), a new User object will be created and coupled to the Profile. So my question is:

How do I make the validation of a serializer check if the edit of a nested object is valid rather than checking if a new one can be created?


Solution

  • I managed to find something of a workaround on my own. Using the source property of serializer fields, one is able to add certain properties from related objects to the serializer. In my case, I was able to add the following lines to the ProfileSerializer for the functionalities I need:

        first_name = serializers.CharField(max_length=30, source='user.first_name', required=False)
        last_name = serializers.CharField(max_length=150, source='user.last_name', required=False)
        email = serializers.CharField(max_length=255, source='user.email')
    

    There are more properties to User, but for my current requirements I don't need them and this works fine (though it would be tedious to add such a line for all properties if you need all attributes). It does require a custom implementation of both create and update functions, but I assume one would need those anyway. Personally I used:

        def create(self, validated_data):
            user_data = validated_data.pop('user')
            user = User.objects.create(**user_data)
            user.save()
            instance = ...
            return instance
    
        def update(self, instance, validated_data):
            user_data = validated_data.pop('user')
            user = instance.user
            for k,v in user_data.items():
                setattr(user, k, v)
            user.save()
            ...
            return instance
    
    

    Note: django-rest automatically groups properties with a shared source object into a nested object: {'user': {'email': ..., 'first_name': ..., 'last_name': ...}, 'phone_number': ...} (where phone_number is a profile property)