Djoser注册报错ValueError:需为CustomUser.native_city传入City实例
问题描述
使用Djoser实现JWT注册逻辑时触发错误:
ValueError: Cannot assign "OrderedDict([('city_name', 'New York')])": "CustomUser.native_city" must be a "City" instance.
尝试继承serializers.ModelSerializer后,用户无法通过含uid和token的激活链接完成账号激活,排查许久未解决,请求帮助。
相关代码
models.py
class City(models.Model): city_name = models.CharField(unique=True) class Meta: ordering = ('id',) def __str__(self): return self.city_name class CustomUser(AbstractUser): email = models.EmailField(unique=True, validators=[validate_email]) native_city = models.ForeignKey( City, on_delete=models.CASCADE, blank=True, null=True ) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] class Meta: constraints = [ models.UniqueConstraint( fields=('email', 'username'), name='unique_username_email') ] def __str__(self): return self.email
serializers.py
class CitySerializer(serializers.ModelSerializer): class Meta: model = City fields = '__all__' class CustomUserCreateSerializer(UserCreateSerializer): email = serializers.EmailField( validators=[UniqueValidator(queryset=CustomUser.objects.all())] ) native_city = CitySerializer() class Meta: model = CustomUser fields = ('id', 'username', 'email', 'password', 'first_name', 'last_name', 'native_city') def create(self, validated_data): native_city_data = validated_data.pop('native_city') city_instance, _ = City.objects.get_or_create(city_name=native_city_data) city_instance.save() user = CustomUser.objects.create(native_city=city_instance, **validated_data) user.save() return user class CustomUserSerializer(UserSerializer): native_city = CitySerializer() class Meta: model = CustomUser fields = ('id', 'username', 'email', 'first_name', 'last_name', 'native_city')
完整报错栈
Internal Server Error: /auth/users/ djangoweatherremider-web-1 | Traceback (most recent call last): djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner djangoweatherremider-web-1 | response = get_response(request) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response djangoweatherremider-web-1 | response = wrapped_callback(request, *callback_args, **callback_kwargs) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 56, in wrapper_view djangoweatherremider-web-1 | return view_func(*args, **kwargs) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/viewsets.py", line 125, in view djangoweatherremider-web-1 | return self.dispatch(request, *args, **kwargs) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch djangoweatherremider-web-1 | response = self.handle_exception(exc) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception djangoweatherremider-web-1 | self.raise_uncaught_exception(exc) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception djangoweatherremider-web-1 | raise exc djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch djangoweatherremider-web-1 | response = handler(request, *args, **kwargs) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/mixins.py", line 18, in create djangoweatherremider-web-1 | serializer.is_valid(raise_exception=True) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/serializers.py", line 227, in is_valid djangoweatherremider-web-1 | self._validated_data = self.run_validation(self.initial_data) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/rest_framework/serializers.py", line 429, in run_validation djangoweatherremider-web-1 | value = self.validate(value) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/djoser/serializers.py", line 71, in validate djangoweatherremider-web-1 | user = User(**attrs) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 543, in __init__ djangoweatherremider-web-1 | _setattr(self, field.name, rel_obj) djangoweatherremider-web-1 | File "/usr/local/lib/python3.10/site-packages/django/db/models/fields/related_descriptors.py", line 266, in __set__ djangoweatherremider-web-1 | raise ValueError( djangoweatherremider-web-1 | ValueError: Cannot assign "OrderedDict([('city_name', 'New York')])": "CustomUser.native_city" must be a "City" instance. djangoweatherremider-web-1 | [30/Aug/2023 10:06:42] "POST /auth/users/ HTTP/1.1" 500 16728
解决方案
错误根源
- Djoser的
validate方法会提前尝试用验证后的属性创建User实例,但native_city此时是嵌套序列化返回的OrderedDict,不是City模型实例,导致赋值失败。 - 直接继承
ModelSerializer会丢失Djoser内置的激活、密码重置等核心逻辑,导致激活链接失效。
修复步骤
1. 重写CustomUserCreateSerializer的字段验证逻辑
保留对DjoserUserCreateSerializer的继承,在验证阶段提前将前端传入的城市字典转换为City实例:
class CustomUserCreateSerializer(UserCreateSerializer): email = serializers.EmailField( validators=[UniqueValidator(queryset=CustomUser.objects.all())] ) # 输出用嵌套序列化器,输入允许字典/空值 native_city = CitySerializer(required=False, allow_null=True) class Meta: model = CustomUser fields = ('id', 'username', 'email', 'password', 'first_name', 'last_name', 'native_city') def validate_native_city(self, value): # 处理前端传入的城市字典 if isinstance(value, dict): city_name = value.get('city_name') if not city_name: raise serializers.ValidationError("city_name字段不能为空") # 存在则获取,不存在则创建 city, _ = City.objects.get_or_create(city_name=city_name) return city # 空值或ID直接返回,Django会自动处理ID转实例 return value def create(self, validated_data): # 验证阶段已处理native_city,直接创建用户 user = CustomUser.objects.create(**validated_data) user.set_password(validated_data['password']) user.save() return user
2. 保留Djoser激活逻辑
不要替换继承链为ModelSerializer,必须基于Djoser提供的UserCreateSerializer和UserSerializer扩展,这样才能保留激活邮件生成、链接验证等内置功能。你原有的CustomUserSerializer写法无需修改。
3. 前端传参格式验证
注册请求中native_city需传如下格式:
{ "username": "testuser", "email": "test@example.com", "password": "testpass123", "native_city": { "city_name": "New York" } }
若需要支持直接传城市ID,可在validate_native_city中添加对整数类型的判断逻辑。
验证修复效果
- 发送注册请求,
validate_native_city会将字典转为City实例,Djoser的验证流程可正常创建用户。 - 用户注册后可收到激活邮件,点击含uid和token的链接可完成账号激活。
内容的提问来源于stack exchange,提问作者DmitryTok
相关产品推荐
相关产品推荐

