DRF getting AttributeError while trying to post data
I'm creating a user registration API in DRF using a Profile model with a OneToOne relationship with Django's default User Model. I have created a serializer with User model and defined the fields of Profile as serializer fields.
The problem is when I post the data, though the user and profile are saved in the database I'm getting this error
error AttributeError at /auth/registration Got AttributeError when attempting to get a value for field address on serializer UserRegistrationSerializer. The serializer field might be named incorrectly and not match any attribute or key on the User instance. Original exception text was: 'User' object has no attribute 'address'.
using write_only with address
seems to solve the problem but I want to understand why is this happening and how this view accessing this address field why it ignores the phone_number
and date_of_birth
and doesn't give me errors on those. Any input will be helpful. Thanks!
Here is my code.
models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
date_of_birth = models.DateField(null=True)
phone_number = models.CharField(null=True, max_length=30)
address = models.CharField(max_length=250)
def __str__(self):
return f'Profile of {self.user.username}'
Serializer.py
class UserRegistrationSerializer(serializers.ModelSerializer):
phone_number = serializers.CharField(required=False, max_length=30)
date_of_birth = serializers.DateField(required=False)
address = serializers.CharField(max_length=250)
password = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True, label = 'Confirm Password')
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email', 'phone_number', 'date_of_birth', 'address', 'password', 'password2']
def create(self, validated_data):
phone_number = validated_data.pop('phone_number', None)
date_of_birth = validated_data.pop('date_of_birth', None)
address = validated_data.pop('address', None)
password = validated_data.pop('password', None)
first_name = validated_data.pop('first_name', None)
last_name = validated_data.pop('last_name', None)
user = User (
username = validated_data['username'],
email = validated_data['email'],
first_name = first_name,
last_name = last_name
)
user.set_password(password)
user.save()
Profile.objects.create(
user=user,
date_of_birth=date_of_birth,
address=address,
phone_number=phone_number
)
return user
view.py
class UserRegistrationView(generics.CreateAPIView):
serializer_class = UserRegistrationSerializer
queryset = User.objects.all()
This error occurs because there is no phone_number, date_of_birth, and address belong to User so DRF couldnt access to them.
But when you putting write_only=True
, DRF doesn't try to access them anymore, that's why it seems like write_only fixes the problem.
You could create nester serialiser ProfileSerializer
and use it in your UserRegistrationSerializer
:
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ['date_of_birth','phone_number','address']
class UserRegistrationSerializer(serializers.ModelSerializer):
profile = ProfileSerializer(required=True)
password = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True, label='Confirm Password')
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email', 'password', 'password2', 'profile']
def create(self, validated_data):
profile_data = validated_data.pop('profile')
password = validated_data.pop('password')
user = User.objects.create(**validated_data)
user.set_password(password)
user.save()
Profile.objects.create(
user=user,
date_of_birth=profile_data.get('date_of_birth'),
phone_number=profile_data.get('phone_number'),
address=profile_data.get('address')
)
return user
Also you could use source
attr at serialiser field:
class UserRegistrationSerializer(serializers.ModelSerializer):
phone_number = serializers.CharField(source='profile.phone_number', required=False, max_length=30)
date_of_birth = serializers.DateField(source='profile.date_of_birth', required=False)
address = serializers.CharField(source='profile.address', required=False, max_length=250)
password = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True, label='Confirm Password')
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email', 'phone_number', 'date_of_birth', 'address', 'password', 'password2']
def create(self, validated_data):
profile_data = validated_data.pop('profile', {})
password = validated_data.pop('password')
user = User.objects.create(**validated_data)
user.set_password(password)
user.save()
Profile.objects.create(
user=user,
phone_number=profile_data.get('phone_number'),
date_of_birth=profile_data.get('date_of_birth'),
address=profile_data.get('address')
)
return user
This issue is due to how Django Rest Framework handles fields in serializers and models.
The Root Cause
You're using a ModelSerializer
based on the User
model, but you've added fields phone_number
, date_of_birth
, and address
that belong to the Profile
model, not the User
model.
When DRF serializes or deserializes the User
object, it expects that all fields in the serializer correspond to attributes on the User
instance. Since address
, phone_number
and date_of_birth
are fields on the Profile
model, not the User
model, DRF doesn't know how to access them.
In contrast, when you set write_only=True
for the address
field, it only allows the serializer to accept input for that field without rying to read it from the User
object, wihch is why the error goes away. But this is only a partial solution.
Why only address
causes an error
When you have required=False
, DRF doesn't complain about missing attributes on theUser
model because those fields are not required. This is why you get an error for only address
. If you set it True, you'll get the same error.
Solution
You need to separate the concerns of serializing the User
and Profile
models. The best way to do this is to use a nested serializer for the Profile
fields.
Here's the updated code:
serializers.py
Create a separate ProfileSerialzier
and nest it inside your UserRegisterationSerializer
:
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ['date_of_birth', 'phone_number', 'address']
class UserRegistrationSerializer(serializers.ModelSerializer):
profile = ProfileSerializer()
password = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True, label='Confirm Password')
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email', 'password', 'password2', 'profile']
def create(self, validated_data):
profile_data = validated_data.pop('profile', {})
password = validated_data.pop('password', None)
first_name = validated_data.pop('first_name', None)
last_name = validated_data.pop('last_name', None)
user = User(
username=validated_data['username'],
email=validated_data['email'],
first_name=first_name,
last_name=last_name
)
user.set_password(password)
user.save()
Profile.objects.create(
user=user,
date_of_birth=profile_data.get('date_of_birth', None),
phone_number=profile_data.get('phone_number', None),
address=profile_data.get('address', None),
)
return user
Sample request data
{
"username": "testuser",
"first_name": "Test",
"last_name": "User",
"email": "testuser@example.com",
"password": "password123",
"password2": "password123",
"profile": {
"date_of_birth": "1990-01-01",
"phone_number": "1234567890",
"address": "Test Street"
}
}
I hope this will help you a little.