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)
错误原因分析
- 只读字段无法接收输入:
SalesDetailSerializers中的sparepart是ReadOnlyField,仅用于序列化输出(返回给客户端的响应),不能用于反序列化接收客户端提交的数据。提交的content数据中包含sparepart字段,但序列化器会忽略这个只读字段,导致无法解析出有效的详情数据,最终validated_data中没有content键,触发KeyError。 - 缺少必要的外键输入字段:
Sales_detail模型需要sparepart_id外键值来关联配件,但当前序列化器没有提供可写入的sparepart_id字段,无法接收创建所需的配件主键。 - 测试数据字段不匹配:测试数据中使用
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
相关产品推荐
相关产品推荐

