Django Ninja框架外键赋值报错:需传入模型实例而非ID
问题现象
基于Django 4.1和Ninja 0.19.1开发的项目,通过Swagger或Postman发送POST请求时出现报错:
ValueError: Cannot assign "115": "Offer.currency_to_sell" must be a "Currency" instance.
提交的请求数据:
{ "currency_to_sell_id": 115, "currency_to_buy_id": 116, "user_id": 1, "amount": 100, "exchange_rate": 10 }
相关代码:
api.py接口
@api.post("/add_offer/") async def add_offer(request, payload: OfferIn): offer = await Offer.objects.acreate(**payload.dict()) return {"id": offer.pk}
schemas.py定义
class OfferIn(ModelSchema): class Config: model = Offer model_fields = [ "currency_to_sell", "currency_to_buy", "user", "amount", "exchange_rate", ]
Offer模型
class Offer(models.Model): """Sell currency offer model.""" currency_to_sell = models.ForeignKey( to="Currency", on_delete=models.CASCADE, related_name="currencies_to_sell", verbose_name="Currency to sell", ) currency_to_buy = models.ForeignKey( to="Currency", on_delete=models.CASCADE, related_name="currencies_to_buy", verbose_name="Currency to buy", ) amount = models.DecimalField( decimal_places=2, max_digits=11, blank=False, null=False, verbose_name="Amount" ) exchange_rate = models.DecimalField( decimal_places=2, max_digits=11, blank=False, null=False, verbose_name="Exchange rate", ) user = models.ForeignKey( to=User, on_delete=models.CASCADE, related_name="offers", verbose_name="User" )
改用普通Schema而非ModelSchema时可正常运行:
class OfferIn(Schema): currency_to_sell_id: int = None currency_to_buy_id: int = None user_id: int = None amount: float exchange_rate: float
用户尝试了一种可行方法,但不确定是否正确:
@api.post("/offer/", tags=["Offer"]) async def add_offer(request, payload: OfferIn): currency_to_sell = await Currency.objects.aget(id=payload.currency_to_sell) currency_to_buy = await Currency.objects.aget(id=payload.currency_to_buy) user = await User.objects.aget(id=payload.user) payload.currency_to_sell = currency_to_sell payload.currency_to_buy = currency_to_buy payload.user = user offer = await Offer.objects.acreate(**payload.dict()) return {"id": offer.pk}
错误原因
ModelSchema生成的OfferIn类,默认会将模型中的ForeignKey字段映射为接收对应模型实例的类型,但你提交的是xxx_id格式的纯数字ID。当直接把这些ID通过**payload.dict()传递给acreate时,Django ORM会尝试把数字赋值给需要模型实例的ForeignKey字段,从而触发报错。
而普通Schema中定义的xxx_id字段,Django ORM支持直接通过后缀_id的字段名赋值ForeignKey,因此可以正常运行。
解决方案
方案1:调整请求数据格式(推荐)
Django Ninja的ModelSchema支持直接接收ForeignKey对应的ID值,只需将请求数据中的xxx_id字段名改为模型中定义的字段名(去掉_id后缀):
{ "currency_to_sell": 115, "currency_to_buy": 116, "user": 1, "amount": 100, "exchange_rate": 10 }
此时无需修改任何代码,原有的OfferIn和接口逻辑即可正常工作——Ninja会自动将ID转换为对应的模型实例。
方案2:修改ModelSchema,支持ID字段
如果必须保留xxx_id的请求字段格式,可以在OfferIn中显式定义ID字段,同时保留模型字段的映射:
class OfferIn(ModelSchema): currency_to_sell_id: int currency_to_buy_id: int user_id: int class Config: model = Offer model_fields = [ "amount", "exchange_rate", ]
这样提交xxx_id格式的数据时,Schema会正确解析ID值,Django ORM也能通过xxx_id字段直接赋值ForeignKey。
方案3:手动转换实例(优化你的方法)
你手动查询模型实例的方法是可行的,但建议添加异常处理,避免因ID不存在导致的崩溃,同时更清晰地传递参数:
from django.core.exceptions import ObjectDoesNotExist from ninja import HttpError @api.post("/offer/", tags=["Offer"]) async def add_offer(request, payload: OfferIn): try: currency_to_sell = await Currency.objects.aget(id=payload.currency_to_sell) currency_to_buy = await Currency.objects.aget(id=payload.currency_to_buy) user = await User.objects.aget(id=payload.user) except ObjectDoesNotExist as e: raise HttpError(404, str(e)) offer = await Offer.objects.acreate( currency_to_sell=currency_to_sell, currency_to_buy=currency_to_buy, user=user, amount=payload.amount, exchange_rate=payload.exchange_rate ) return {"id": offer.pk}
内容的提问来源于stack exchange,提问作者Vitalii Mytenko

