from collections import OrderedDict
from rest_framework.fields import SkipField
from rest_framework.relations import PKOnlyObject
class NbHookSerializer:
"""
Mixin:给 DRF Serializer 增加“字段钩子”机制,
只需定义 nb_<field_name>(),就能单独定制某个字段的输出。
"""
def to_representation(self, instance):
# ① 用有序字典保证输出顺序和 Meta.fields 一致
ret = OrderedDict()
# ② DRF 内部属性,包含所有可以读的字段对象列表
fields = self._readable_fields
# ③ 遍历每个字段
for field in fields:
hook_name = f'nb_{field.field_name}'
# ④ 如果用户定义了 nb_<字段名> 方法,就调用它
if hasattr(self, hook_name):
# 调用自定义钩子,传入 model 实例,取它的返回值作为输出
value = getattr(self, hook_name)(instance)
ret[field.field_name] = value
else:
# ⑤ 没有钩子时,执行 DRF 默认的取值和序列化逻辑
try:
# get_attribute 负责从 instance 上拿到属性或 Related 对象
attribute = field.get_attribute(instance)
except SkipField:
# 如果这个字段应被跳过(nested serializer 返回 None),就 continue
continue
# ⑥ 如果拿到的是 PKOnlyObject,取 .pk 来判空;否则直接用 attribute
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
# 如果值为 None,就直接输出 None
ret[field.field_name] = None
else:
# ⑦ 调用该字段类型自带的 to_representation,将 attribute 转成基本类型
ret[field.field_name] = field.to_representation(attribute)
# ⑧ 返回最终的 OrderedDict,DRF 会再把它转成 JSON
return ret
第 1–3 行:引入
OrderedDict
、SkipField
、PKOnlyObject
,分别为保证输出顺序、跳过字段信号,以及处理延迟外键。class NbHookSerializer
:定义一个 Mixin,专门重写to_representation
。步骤 ①:初始化有序结果容器
ret
。步骤 ②:拿到 Serializer 在初始化时收集的可读字段列表。
步骤 ③:对每个字段循环。
步骤 ④:优先检查并调用钩子方法
nb_<field_name>(self, instance)
,只定制该字段。步骤 ⑤:若无钩子,则按 DRF 标准流程取值并序列化。
步骤 ⑥:PKOnlyObject 要取其
.pk
判空。步骤 ⑦:调用字段自身的
to_representation
,例如CharField
输出字符串,DateTimeField
输出 ISO 格式时间等。步骤 ⑧:返回最终结果。
2. 详细使用示例
假设我们有一个简单的博客系统,模型如下:
# models.py
from django.db import models
from django.contrib.auth.models import User
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
is_public = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
tags = models.ManyToManyField('Tag', blank=True)
class Tag(models.Model):
name = models.CharField(max_length=50)
我们想实现的序列化输出:
id
、title
、created
:按 DRF 默认格式输出author
:只输出用户名summary
:动态生成,取content
前 30 字再加省略号tag_list
:把所有关联的 Tag 名拼成一个逗号分隔的字符串public_label
:如果is_public
为真,显示"公开"
,否则"私密"
定义 Serializer
from rest_framework import serializers
from .models import Article, Tag
from .nb_hook import NbHookSerializer # 假设把上面代码放在 nb_hook.py
class ArticleSerializer(NbHookSerializer, serializers.ModelSerializer):
# 手动声明两个字段:summary 和 tag_list
summary = serializers.CharField(read_only=True)
tag_list = serializers.CharField(read_only=True)
public_label = serializers.CharField(read_only=True)
# author 用 source 指定取 username
author = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'summary', 'content', 'author',
'tag_list', 'public_label', 'created']
read_only_fields = ['id', 'summary', 'author', 'tag_list', 'public_label', 'created']
# 钩子:为 summary 字段生成内容
def nb_summary(self, instance):
txt = instance.content or ''
return txt[:30] + ('…' if len(txt) > 30 else '')
# 钩子:为 tag_list 拼接字符串
def nb_tag_list(self, instance):
tags = instance.tags.all().values_list('name', flat=True)
return ','.join(tags)
# 钩子:为 public_label 提供中英文标签
def nb_public_label(self, instance):
return "公开" if instance.is_public else "私密"
使用说明
混入顺序:
NbHookSerializer
必须写在ModelSerializer
前面,确保它的to_representation
生效。声明字段:我们手动声明了
summary
、tag_list
、public_label
这三个自定义字段,并标为read_only
,这样它们只能输出,不参与反序列化。钩子方法:按照字段名,分别定义
nb_summary
、nb_tag_list
、nb_public_label
,返回值即为该字段在输出里的最终值。其它字段:
id
、title
、content
、author
、created
均按 DRF 默认逻辑处理——author
因为手动source='author.username'
,会输出 username。
测试输出
# 在 views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer
class ArticleDetail(APIView):
def get(self, request, pk):
art = Article.objects.get(pk=pk)
data = ArticleSerializer(art).data
return Response(data)
假设数据库里有一条记录:
Article(
id=5,
title="DRF 钩子示例",
content="这是一个演示 NbHookSerializer 用法的示例内容,用来截取前面的字符。",
author=User(username='alice'),
is_public=False,
created="2025-05-15T08:00:00Z",
tags=[Tag(name='drf'), Tag(name='hook')]
)
访问 GET /api/articles/5/
,你会得到:
{
"id": 5,
"title": "DRF 钩子示例",
"summary": "这是一个演示 NbHookSerializer 用法…",
"content": "这是一个演示 NbHookSerializer 用法的示例内容,用来截取前面的字符。",
"author": "alice",
"tag_list": "drf,hook",
"public_label": "私密",
"created": "2025-05-15T08:00:00Z"
}
3. 小结
核心功能:一个 mixin,让你只需定义
nb_<字段名>
方法,就能针对单个字段自定义序列化输出,而无需重写整个to_representation
。使用步骤:
在自定义序列化器类中继承
NbHookSerializer, ModelSerializer
。在
Meta.fields
列出所有字段(包括钩子字段)。对于普通模型字段,DRF 正常处理;对于钩子字段,定义对应的
nb_<字段名>(self, instance)
即可。
优势:更模块化、更易维护、减少对底层逻辑的侵入。
评论