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

Django Rest Framework嵌套序列化器创建测试遇KeyError: 'content'

解决DRF嵌套序列化器创建销售数据时的KeyError: 'content'问题

问题概述

编写创建销售数据的测试用例时,运行python manage.py test持续抛出KeyError: 'content'。需求是验证用户能否创建包含详情的销售数据,参考DRF可写嵌套序列化器文档实现,但测试始终失败。期望响应中sparepart字段显示对应配件名称。

相关代码

models.py

# abstract base table for transactions
class Base_transaction(models.Model):
    is_paid_off = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    pass

    class Meta:
        abstract = True


# sales table to store surface level sales information
# consist of sales_id as pk, customer_name, sub_total, discount, user_id,
# total, is_paid_off, created_at, updated_at
class Sales(Base_transaction):
    sales_id = models.BigAutoField(
        primary_key=True,
        unique=True
    )
    customer_name = models.CharField(max_length=25, blank=True)
    customer_contact = models.CharField(max_length=13, blank=True)
    user_id = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        null=True,
        db_column='user_id'
    )

    def __str__(self) -> str:
        return f'{self.sales_id} at {self.created_at} | Lunas={self.is_paid_off}'

    class Meta:
        db_table = 'sales'


# sales_detail table store the detail of sales per sparepart
# consist of sales_detail_id as pk, quantity, individual_price, total_price
# sales_id
class Sales_detail(models.Model):
    sales_detail_id = models.BigAutoField(
        primary_key=True,
        unique=True
    )
    quantity = models.PositiveSmallIntegerField()
    is_grosir = models.BooleanField(default=False)
    sales_id = models.ForeignKey(
        Sales,
        on_delete=models.CASCADE,
        db_column='sales_id'
    )
    sparepart_id = models.ForeignKey(
        'Sparepart',
        on_delete=models.SET_NULL,
        null=True,
        db_column='supplier_id'
    )

    def __str__(self) -> str:
        return f'{self.sales_id} - {self.sparepart_id}'

serializers.py

class SalesDetailSerializers(serializers.ModelSerializer):
    sparepart = serializers.ReadOnlyField(source='sparepart_id.name')

    class Meta:
        model = Sales_detail
        fields = ['sales_detail_id', 'sparepart', 'quantity', 'is_grosir']


class SalesSerializers(serializers.ModelSerializer):
    content = SalesDetailSerializers(many=True, source='sales_detail_set')

    class Meta:
        model = Sales
        fields = ['sales_id', 'customer_name', 'customer_contact', 'is_paid_off', 'content']

    def create(self, validated_data):
        details = validated_data.pop('content')

        sales = Sales.objects.create(**validated_data)
        for detail in details:
            Sales_detail.objects.create(sales_id=sales, **detail)
        return sales

test.py

class SalesAddTestCase(APITestCase):
    sales_url = reverse('sales_add')

    def setUp(self) -> None:
        # Setting up sparepart data
        for i in range(3):
            Sparepart.objects.create(
                name=f'random name{i}',
                partnumber=f'0Y3AD-FY{i}',
                quantity=50,
                motor_type='random m',
                sparepart_type='random s',
                price=5400000,
                grosir_price=5300000,
                brand_id=None
            )

        self.spareparts = Sparepart.objects.all()

        # Creating data that gonna be use as input
        self.data = {
            'customer_name': 'someone',
            'customer_contact': '085634405602',
            'is_paid_off': False,
            'content': [
                {
                    'sparepart': self.spareparts[1].sparepart_id,
                    'quantity': 1,
                    'is_grosir': False,
                },
                {
                    'sparepart': self.spareparts[0].sparepart_id,
                    'quantity': 30,
                    'is_grosir': True,
                }
            ]
        }

    def test_user_successfully_add_sales(self) -> None:
        """
        Ensure user can add new sales data with it's content
        """
        response = self.client.post(self.sales_url, self.data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data['customer_name'], 'someone')
        self.assertEqual(len(response.data['content']), 2)

错误原因分析

  1. 只读字段无法接收输入:SalesDetailSerializers中的sparepart是ReadOnlyField,仅用于序列化输出(返回给客户端的响应),不能用于反序列化接收客户端提交的数据。提交的content数据中包含sparepart字段,但序列化器会忽略这个只读字段,导致无法解析出有效的详情数据,最终validated_data中没有content键,触发KeyError。
  2. 缺少必要的外键输入字段:Sales_detail模型需要sparepart_id外键值来关联配件,但当前序列化器没有提供可写入的sparepart_id字段,无法接收创建所需的配件主键。
  3. 测试数据字段不匹配:测试数据中使用sparepart字段传递值,但序列化器需要的是sparepart_id,字段名不匹配导致数据无法被正确解析。

解决方案

1. 修改序列化器,添加可写入字段

调整SalesDetailSerializers,添加可写入的sparepart_id字段,同时保留只读的sparepart字段用于输出展示配件名称:

class SalesDetailSerializers(serializers.ModelSerializer):
    sparepart = serializers.ReadOnlyField(source='sparepart_id.name')
    # 添加可写入的sparepart_id字段,用于接收输入
    sparepart_id = serializers.PrimaryKeyRelatedField(queryset=Sparepart.objects.all())

    class Meta:
        model = Sales_detail
        fields = ['sales_detail_id', 'sparepart', 'sparepart_id', 'quantity', 'is_grosir']
        # 指定哪些字段仅用于输出,避免返回给客户端
        read_only_fields = ['sales_detail_id', 'sparepart']

2. 调整测试数据的字段名

将测试数据中的sparepart改为sparepart_id,传入配件的主键值:

self.data = {
    'customer_name': 'someone',
    'customer_contact': '085634405602',
    'is_paid_off': False,
    'content': [
        {
            'sparepart_id': self.spareparts[1].pk,
            'quantity': 1,
            'is_grosir': False,
        },
        {
            'sparepart_id': self.spareparts[0].pk,
            'quantity': 30,
            'is_grosir': True,
        }
    ]
}

3. 修正模型中的数据库列名(可选)

注意到Sales_detail的sparepart_id外键指定了db_column='supplier_id',这可能是笔误,建议改为db_column='sparepart_id',避免数据库字段名混淆:

sparepart_id = models.ForeignKey(
    'Sparepart',
    on_delete=models.SET_NULL,
    null=True,
    db_column='sparepart_id'  # 修正列名
)

验证结果

修改完成后重新运行python manage.py test,测试应该能通过,且响应中sparepart字段会正确显示配件名称,符合预期格式。

内容的提问来源于stack exchange,提问作者sayyinside

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 15:30:59