Handling user registration and subsequent profile creation

I have a custom User model with a role field and respective [Role]Profile models:

class User(AbstractUser):
    role = models.CharField()
    # Other fields

class TeacherProfile(models.Model):
    # profile_picture and other fields

class StudentProfile(models.Model):
    # for now just a reference to the user model
    user = models.OneToOneField(User, ...)

These models have their respective serializers. I am using the djoser library for handling user authentication. Now the default way I use to handle user creation and profile management would be:

  1. Client sends user payload only - stuff like name, email, role etc.
  2. Use post_save signal to create the appropriate profile object by determining the role field.
  3. Then to upload profile related fields, simply send a PATCH request for that profile.

The issue is the client wants to send the whole thing at once. They have a registration form which contains auth fields and also profile fields like profile_pic or about field or whatever. This is what ChatGPT suggested:

class CustomUserCreateSerializer(UserCreateSerializer):
    teacher_profile = TeacherProfileSerializer(required=False)
    student_profile = StudentProfileSerializer(required=False)

    class Meta(UserCreateSerializer.Meta):
        model = User
        fields = (
            "id",
            "email",
            "password",
            "role",
            "first_name",
            "last_name",
            "teacher_profile",
            "student_profile",
        )

    def create(self, validated_data):
        teacher_profile_data = validated_data.pop("teacher_profile", None)
        student_profile_data = validated_data.pop("student_profile", None)

        user = super().create(validated_data)

        # Signals will create the profile automatically, we just update it if data was provided
        if user.role == User.TEACHER and teacher_profile_data:
            profile, created = TeacherProfile.objects.get_or_create(user=user)
            for attr, value in teacher_profile_data.items():
                setattr(profile, attr, value)
            profile.save()
        elif user.role == User.STUDENT and student_profile_data:
            profile, created = StudentProfile.objects.get_or_create(user=user)
            for attr, value in student_profile_data.items():
                setattr(profile, attr, value)
            profile.save()

        return user


class CustomUserCreatePasswordRetypeSerializer(UserCreatePasswordRetypeSerializer):
    teacher_profile = TeacherProfileSerializer(required=False)
    student_profile = StudentProfileSerializer(required=False)

    class Meta(UserCreatePasswordRetypeSerializer.Meta):
        model = User
        fields = (
            "id",
            "email",
            "password",
            "role",
            "first_name",
            "last_name",
            "teacher_profile",
            "student_profile",
        )

    def create(self, validated_data):
        teacher_profile_data = validated_data.pop("teacher_profile", None)
        student_profile_data = validated_data.pop("student_profile", None)

        user = super().create(validated_data)

        if user.role == User.TEACHER and teacher_profile_data:
            profile, created = TeacherProfile.objects.get_or_create(user=user)
            for attr, value in teacher_profile_data.items():
                setattr(profile, attr, value)
            profile.save()
        elif user.role == User.STUDENT and student_profile_data:
            profile, created = StudentProfile.objects.get_or_create(user=user)
            for attr, value in student_profile_data.items():
                setattr(profile, attr, value)
            profile.save()

        return user


class CustomUserSerializer(UserSerializer):
    class Meta(UserSerializer.Meta):
        model = User
        fields = ("id", "email", "role", "first_name", "last_name")

I don't know if this is the right way to go, for some reason it feels wrong. Can anyone suggest how I should handle this. Now the payload looks like this:

{
  "email": "user@example.com",
  "password": "string",
  "role": "admin",
  "first_name": "string",
  "last_name": "string",
  "teacher_profile": {
    "profile_picture": "string",
    "professional_title": "string",
    "location": "string",
    "about": "string",
    "education": "string",
    "achievements": "string",
    "consultation_rate": "51251202",
    "user": 0
  },
  "student_profile": {
    "user": 0
  },
  "re_password": "string"
}

You’re not wrong to feel this is a bit off. The main issue is that you’re mixing signals and serializer logic for the same responsibility (profile creation), which makes things harder to reason about.

If you’re already handling nested data in the serializer, it’s cleaner to do everything there and drop the signal.


What I’d change

  1. Only accept one profile in the payload
    A user can only have one role, so there’s no need to send both teacher_profile and student_profile.

  2. Handle profile creation inside create()
    This keeps everything in one place and avoids get_or_create + signal duplication.

  3. Don’t let the client send user inside profile data
    You already have the user instance after creation.


Example

class CustomUserCreateSerializer(UserCreateSerializer):
    teacher_profile = TeacherProfileSerializer(required=False)
    student_profile = StudentProfileSerializer(required=False)

    class Meta(UserCreateSerializer.Meta):
        model = User
        fields = (
            "id",
            "email",
            "password",
            "role",
            "first_name",
            "last_name",
            "teacher_profile",
            "student_profile",
        )

    def validate(self, data):
        role = data.get("role")

        if role == "teacher" and not data.get("teacher_profile"):
            raise serializers.ValidationError("Teacher profile required")

        if role == "student" and not data.get("student_profile"):
            raise serializers.ValidationError("Student profile required")

        return data

    def create(self, validated_data):
        teacher_data = validated_data.pop("teacher_profile", None)
        student_data = validated_data.pop("student_profile", None)

        user = super().create(validated_data)

        if user.role == "teacher":
            TeacherProfile.objects.create(user=user, **teacher_data)

        elif user.role == "student":
            StudentProfile.objects.create(user=user, **student_data)

        return user

Payload example

{
  "email": "user@example.com",
  "password": "string",
  "role": "teacher",
  "first_name": "string",
  "last_name": "string",
  "teacher_profile": {
    "profile_picture": "string",
    "about": "string"
  }
}

Вернуться на верх