djangodjango-modelsserializationdjango-rest-frameworkdjoser

How to use a single serializer to save multiple models


Problem : I’m unable to save the both models in database from a single serialzer.

Description : I’m using djoser auth library for registration and authentication. for that i’m using custom django auth user and mapped it with userprofile model. Since i’m using userprofile along with the django user, i have to use custom register serializer.

My Models

class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_("email address"), unique=True)
    
    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []
    
    objects = CustomUserManager()
    
class UserProfile(models.Model):
    user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name="userProfile")
    user_guid = models.UUIDField(
        default=uuid.uuid4,
        unique=True,
        editable=False
    )
    user_type = models.IntegerField(choices=UserType.choices(), default=UserType.BUYER)
    wallet_balance = models.FloatField()
    created_at = models.DateTimeField(auto_now=True)
    updated_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['created_at' ]  # Corrected the ordering syntax

My Serializer

CustomUser = get_user_model()

class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = ['user_guid', 'user_type', 'wallet_balance', 'created_at', 'updated_at']
        read_only_fields = ['user_guid', 'created_at', 'updated_at']

class CustomUserCreateSerializer(DjoserUserCreateSerializer):
    userProfile = UserProfileSerializer()

    class Meta:
        model = CustomUser
        fields = ['id', 'email', 'password', 'userProfile']
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user_profile_data = validated_data.pop('userProfile')
        user = CustomUser.objects.create_user(**validated_data)
        

        UserProfile.objects.create(user=user, **user_profile_data)
        return user`


    `AUTH_USER_MODEL = 'users.CustomUser'
    DJOSER = {
    'SERIALIZERS': {
        'user_create': 'users.v1.serializers.CustomUserCreateSerializer',
        'user': 'users.v1.serializers.CustomUserCreateSerializer',
        'current_user': 'users.v1.serializers.CustomUserCreateSerializer',
    }
}

Error : raise ValueError( ValueError: Cannot assign “{‘user_type’: 1, ‘wallet_balance’: 0.0}”: “CustomUser.userProfile” must be a “UserProfile” instance.


Solution

    1. You don't need to provide CustomUserCreateSerializer.Meta.extra_kwargs for password, because the "write_only" param already set in djoser.serializers.UserCreateSerializer;
    2. You customized create method, like in this docs, but you need to customize validate method too.
    from django.contrib.auth.password_validation import validate_password
    from django.core import exceptions as django_exceptions
    from djoser.serializers import UserCreateSerializer
    from rest_framework import serializers
    from rest_framework.settings import api_settings
    
    from .models import UserProfile, CustomUser
    
    
    class UserProfileSerializer(serializers.ModelSerializer):
        class Meta:
            model = UserProfile
            fields = ['user_guid', 'wallet_balance', 'created_at', 'updated_at']
            read_only_fields = ['user_guid', 'created_at', 'updated_at']
    
    
    class CustomUserCreateSerializer(UserCreateSerializer):
        user_profile = UserProfileSerializer()
    
        class Meta:
            model = CustomUser
            fields = ['id', 'email', 'password', 'user_profile']
    
        def create(self, validated_data):
            profile_data = validated_data.pop('user_profile')
            user = CustomUser.objects.create_user(**validated_data)
            UserProfile.objects.create(user=user, **profile_data)
            return user
    
        def validate(self, attrs):
            profile_data = attrs.pop("user_profile")
            user = CustomUser(attrs)
            profile = UserProfile(user=user, **profile_data)
            password = attrs.get("password")
            try:
                validate_password(password, user)
            except django_exceptions.ValidationError as e:
                serializer_error = serializers.as_serializer_error(e)
                raise serializers.ValidationError(
                    {"password": serializer_error[api_settings.NON_FIELD_ERRORS_KEY]}
                )
            attrs["user_profile"] = profile_data
            return attrs
    

    this worked fine on my machine

    python manage.py shell
    
    Python 3.11.9 (tags/v3.11.9:de54cf5, Apr  2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    (InteractiveConsole)
    >>> from my_app.serializers import CustomUserCreateSerializer                                                     
    >>> data = {"email": "lengthylyova@gmail.com", "password": "SomeInsanePassword123", "user_profile": {"wallet_balance": 0.0}}
    >>> serializer = CustomUserCreateSerializer(data=data)
    >>> serializer.is_valid(raise_exception=True) 
    True
    >>> serializer.save()
    <CustomUser: lengthylyova@gmail.com>
    

    UserProfile instance also created

    sqlite3 db.sqlite3
    
    sqlite> select * from my_app_userprofile;
    1|3c6a03729d6a41f4b719b7a702b06507|0.0|2024-07-30 00:01:36.824563|2024-07-30 00:01:36.824563|1