You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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')]} 

待解决问题

  1. 为什么执行w2_deser.is_valid()时返回False?
  2. 如何解决对应的w2_deser.errors报错(提示传入的User对象不是合法UUID)?曾尝试设置user_id=cls.u1.user_id,仍未解决该问题。
  3. 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,会自动把浮点数转为整数,不会主动拦截带小数的值。你之前写的自定义验证器未生效,大概率是没有在序列化器/模型层正确注册,或者没有触发校验逻辑。

修复方案

两层同时校验即可解决:

  1. 序列化器层开启严格整数校验:显式定义times字段,开启严格模式,直接拦截非整数输入:
class VacationSerializer(serializers.ModelSerializer):
    times = serializers.IntegerField(strict=True, min_value=1, max_value=15)
    class Meta:
        model = Vacation
        fields = '__all__'
  1. 模型层加兜底校验:在模型的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 00:47:06