如何在Django Ninja的ModelSchema中包含字段显示文本(如get_foo_display)
如何在Django Ninja接口的返回结果中,同时展示IntegerField的数值和对应的choices显示文本(即Django模型的get_foo_display()方法返回值)?
当前场景是:Django模型中的IntegerField定义了choices集合,使用Django Ninja的ModelSchema更新模型后,接口仅返回数值(如1、2、3),希望同时返回对应的显示文本(如'unhelpful')。
示例代码
models.py
from django.db import models from django.utils.translation import gettext_lazy as _ from django.conf import settings class PracticeSession(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=False,related_name="practice_sessions", db_index=True, ) class RATING_CHOICES(models.IntegerChoices): UNHELP = 1, _('unhelpful') NEITHER = 2, _('neither helpful nor unhelpful') HELP = 3, _('helpful') rating = models.IntegerField(choices=RATING_CHOICES.choices) is_practice_done = models.BooleanField(default=False)
api.py
from ninja import NinjaAPI, Schema, ModelSchema from ninja.security import django_auth from practice.models import PracticeSession api = NinjaAPI(csrf=True, auth=django_auth) class UpdatePracticeSessionSchema(ModelSchema): class Meta: model = PracticeSession fields = ['is_practice_done', 'rating'] @api.put( "member/{member_id}/practice_session/{sesh_id}/update/", response={200: UpdatePracticeSessionSchema, 404: Schema} ) def update_practice_sesh(request, member_id: int, sesh_id: int, data: UpdatePracticeSessionSchema): try: practice_sesh = PracticeSession.objects.get(pk=sesh_id) practice_sesh.is_practice_done = data.is_practice_done practice_sesh.rating = data.rating practice_sesh.save() return practice_sesh except Exception as e: print(f"Error in update_practice_sesh: {str(e)}") return 404, {'message': f'Error: {str(e)}'}
已尝试的方法
曾在UpdatePracticeSessionSchema的Meta类前添加rating_choices = PracticeSession.rating.field.choices,但触发Pydantic错误:
pydantic.errors.PydanticUserError: A non-annotated attribute was detected:
rating_choices = [(1, 'unhelpful'), (2, 'neither helpful nor unhelpful'), (3, 'helpful')]. All model fields require a type annotation; ifrating_choicesis not meant to be a field, you may be able to resolve this error by annotating it as aClassVaror updatingmodel_config['ignored_types'].
这种方式仅返回所有选项的映射,还需前端额外处理提取对应文本,不符合需求——希望接口直接返回当前数值对应的显示文本。
解决方案
方法1:拆分更新与响应Schema(推荐)
更新操作只需接收数值,返回数据需包含显示文本,分开定义两个Schema更清晰:
from ninja import NinjaAPI, Schema, ModelSchema from ninja.security import django_auth from practice.models import PracticeSession from pydantic import Field api = NinjaAPI(csrf=True, auth=django_auth) # 用于接收更新请求的Schema class UpdatePracticeSessionSchema(ModelSchema): class Meta: model = PracticeSession fields = ['is_practice_done', 'rating'] # 用于返回响应数据的Schema,新增显示文本字段 class PracticeSessionResponseSchema(ModelSchema): rating_display: str = Field(..., description="评分对应的显示文本") class Meta: model = PracticeSession fields = ['is_practice_done', 'rating'] @classmethod def from_orm(cls, obj): # 调用模型的get_rating_display()方法获取显示文本 return cls( **super().from_orm(obj).dict(), rating_display=obj.get_rating_display() ) @api.put( "member/{member_id}/practice_session/{sesh_id}/update/", response={200: PracticeSessionResponseSchema, 404: Schema} ) def update_practice_sesh(request, member_id: int, sesh_id: int, data: UpdatePracticeSessionSchema): try: practice_sesh = PracticeSession.objects.get(pk=sesh_id) practice_sesh.is_practice_done = data.is_practice_done practice_sesh.rating = data.rating practice_sesh.save() return practice_sesh except Exception as e: print(f"Error in update_practice_sesh: {str(e)}") return 404, {'message': f'Error: {str(e)}'}
方法2:在单Schema中添加计算字段
如果不想拆分Schema,可以直接在原Schema中新增字段,通过验证器填充显示文本:
from ninja import NinjaAPI, Schema, ModelSchema from ninja.security import django_auth from practice.models import PracticeSession from pydantic import Field, field_validator from typing import Any api = NinjaAPI(csrf=True, auth=django_auth) class UpdatePracticeSessionSchema(ModelSchema): rating_display: str = Field(None, description="评分对应的显示文本") class Meta: model = PracticeSession fields = ['is_practice_done', 'rating'] @field_validator('rating_display', mode='before') def populate_rating_display(cls, v, values: dict[str, Any]): # 从模型实例转换时(响应阶段)填充显示文本,接收请求时忽略 if isinstance(values.get('__object__'), PracticeSession): return values['__object__'].get_rating_display() return v @api.put( "member/{member_id}/practice_session/{sesh_id}/update/", response={200: UpdatePracticeSessionSchema, 404: Schema} ) def update_practice_sesh(request, member_id: int, sesh_id: int, data: UpdatePracticeSessionSchema): try: practice_sesh = PracticeSession.objects.get(pk=sesh_id) practice_sesh.is_practice_done = data.is_practice_done practice_sesh.rating = data.rating practice_sesh.save() return practice_sesh except Exception as e: print(f"Error in update_practice_sesh: {str(e)}") return 404, {'message': f'Error: {str(e)}'}
方法3:封装SerializerExtension(批量场景)
如果多个模型都需要返回choices显示文本,可以封装成Django Ninja扩展实现批量处理:
from ninja import NinjaAPI, Schema, ModelSchema from ninja.security import django_auth from practice.models import PracticeSession from ninja.extensions import SerializerExtension class ChoiceDisplayExtension(SerializerExtension): def serialize_field(self, field, value, obj): # 自动检测模型是否有get_xxx_display方法,有则添加xxx_display字段 if hasattr(obj, f'get_{field.name}_display'): self.context['result'][f'{field.name}_display'] = getattr(obj, f'get_{field.name}_display')() return value # 注册扩展到API实例 api = NinjaAPI(csrf=True, auth=django_auth, extensions=[ChoiceDisplayExtension]) class UpdatePracticeSessionSchema(ModelSchema): class Meta: model = PracticeSession fields = ['is_practice_done', 'rating'] @api.put( "member/{member_id}/practice_session/{sesh_id}/update/", response={200: UpdatePracticeSessionSchema, 404: Schema} ) def update_practice_sesh(request, member_id: int, sesh_id: int, data: UpdatePracticeSessionSchema): try: practice_sesh = PracticeSession.objects.get(pk=sesh_id) practice_sesh.is_practice_done = data.is_practice_done practice_sesh.rating = data.rating practice_sesh.save() return practice_sesh except Exception as e: print(f"Error in update_practice_sesh: {str(e)}") return 404, {'message': f'Error: {str(e)}'}
内容的提问来源于stack exchange,提问作者DevinG

