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?
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)