Django REST测试:序列化器外键报错、整数字段接收浮点值问题
问题背景
我正在为Django REST API编写Django模型与序列化器的测试用例。
项目代码
models.py
from django.db import models import uuid class User(models.Model): user_id = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4) first_name = models.CharField(max_length=100) second_name = models.CharField(max_length=100) def getuuid(self): return str(self.user_id) class Workout(models.Model): user_id = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=False, blank=False, default=None, db_column='user_id') hours = models.FloatField() class Meta: constraints = [ models.CheckConstraint( check=models.Q(hours__gte=0), name="Hours should be positive" ) ] class Vacation(models.Model): user_id = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=False, blank=False, default=None, db_column='user_id') times = models.IntegerField() class Meta: constraints = [ models.CheckConstraint( check=models.Q(times__gte=1) & models.Q(times__lte=15), name="Valid range is 1 to 15 inclusive" ) ]
serializers.py
from rest_framework import serializers from .models import User, Workout, Vacation class UserSerializer(serializers.ModelSerializer): class Meta: model= User fields= '__all__' class WorkoutSerializer(serializers.ModelSerializer): class Meta: model= Workout fields= '__all__' class VacationSerializer(serializers.ModelSerializer): class Meta: model= Vacation fields= '__all__'
tests.py
from django.test import TestCase from .models import * from .serializers import * class UserTest(TestCase): @classmethod def setUpTestData(cls): super(UserTest, cls).setUpTestData() cls.u1 = User.objects.create( first_name = "Jack", second_name = "Mueller", ) cls.w1 = Workout.objects.create( hours = 2, user_id = cls.u1 ) cls.t1 = Vacation.objects.create( times = 2.5, user_id = cls.u1 ) cls.w2_val = { "hours":6, "user_id": cls.u1 } cls.w2 = Workout.objects.create(**cls.w2_val) cls.w2_ser = WorkoutSerializer(cls.w2) cls.w2_deser = WorkoutSerializer(data=cls.w2_val) def test_user_model(self): self.assertEquals(len(User.objects.all()), 1) def test_serializer(self): print(self.w2_deser.is_valid()) # return False print(self.w2_deser.errors) # {'user_id': [ErrorDetail(string='“User object (3a598312-5f3d-4adb-b4fc-c415fb024e8a)” is not a valid UUID.', code='invalid')]}
待解决问题
- 为什么执行
w2_deser.is_valid()时返回False? - 如何解决对应的
w2_deser.errors报错(提示传入的User对象不是合法UUID)?曾尝试设置user_id=cls.u1.user_id,仍未解决该问题。 - Vacation类中的
times字段定义为Integer类型,却可以接收浮点值,该现象的成因是什么?要如何修复?曾尝试编写自定义验证器但未生效。
问题解答
1. is_valid()返回False的原因
Django ORM和DRF序列化器的输入预期逻辑不同:
- 调用
Workout.objects.create()时,Django ORM原生支持向外键字段传入模型实例,会自动提取实例主键写入数据库,所以传user_id=cls.u1可以成功创建记录。 - DRF序列化器反序列化(传入
data参数做校验)的场景是处理API接收到的原始JSON数据,这类数据只会包含基础类型(字符串、数字等),不会出现Python模型实例,因此外键字段默认期望接收关联模型的主键值,不会自动做模型实例到主键的转换。你传入的是完整的User对象,序列化器直接把整个对象当成UUID解析,自然返回非法UUID的校验错误。
2. UUID报错的修复方法
反序列化传入数据时,外键字段要传入字符串格式的主键值,不能传模型实例,也不能直接传uuid.UUID类型对象(部分DRF版本不识别UUID类型,只接受字符串格式的UUID)。
修改w2_val的写法即可:
cls.w2_val = { "hours": 6, "user_id": str(cls.u1.user_id) }
注:ForeignKey参数里的
db_column='user_id'仅修改数据库表的列名,不影响序列化器的字段逻辑,不需要调整序列化器字段名。
3. IntegerField接收浮点值的原因与修复方案
成因
- Django模型层默认对IntegerField做隐式类型转换:给字段赋值浮点数时,会自动调用
int()截断小数部分存入数据库(比如传入2.5实际存为2),赋值阶段不会主动抛类型错误。 - 你写的
CheckConstraint是数据库层面的约束,只有模型执行full_clean()或写入数据库时才会触发,直接赋值时不会自动校验。 - DRF的
IntegerField默认配置strict=False,会自动把浮点数转为整数,不会主动拦截带小数的值。你之前写的自定义验证器未生效,大概率是没有在序列化器/模型层正确注册,或者没有触发校验逻辑。
修复方案
两层同时校验即可解决:
- 序列化器层开启严格整数校验:显式定义
times字段,开启严格模式,直接拦截非整数输入:
class VacationSerializer(serializers.ModelSerializer): times = serializers.IntegerField(strict=True, min_value=1, max_value=15) class Meta: model = Vacation fields = '__all__'
- 模型层加兜底校验:在模型的
clean方法中增加类型判断,避免绕过序列化器直接操作ORM时写入非法值:
from django.core.exceptions import ValidationError class Vacation(models.Model): user_id = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=False, blank=False, default=None, db_column='user_id') times = models.IntegerField() def clean(self): if isinstance(self.times, float) and not self.times.is_integer(): raise ValidationError({"times": "times字段必须为整数"}) class Meta: constraints = [ models.CheckConstraint( check=models.Q(times__gte=1) & models.Q(times__lte=15), name="Valid range is 1 to 15 inclusive" ) ]
内容的提问来源于stack exchange,提问作者code_robot
相关产品推荐
相关产品推荐

