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 行:引入 OrderedDictSkipFieldPKOnlyObject,分别为保证输出顺序、跳过字段信号,以及处理延迟外键。

  • 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)

我们想实现的序列化输出:

  • idtitlecreated:按 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 "私密"

使用说明

  1. 混入顺序NbHookSerializer 必须写在 ModelSerializer 前面,确保它的 to_representation 生效。

  2. 声明字段:我们手动声明了 summarytag_listpublic_label 这三个自定义字段,并标为 read_only,这样它们只能输出,不参与反序列化。

  3. 钩子方法:按照字段名,分别定义 nb_summarynb_tag_listnb_public_label,返回值即为该字段在输出里的最终值。

  4. 其它字段idtitlecontentauthorcreated 均按 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

  • 使用步骤

    1. 在自定义序列化器类中继承 NbHookSerializer, ModelSerializer

    2. Meta.fields 列出所有字段(包括钩子字段)。

    3. 对于普通模型字段,DRF 正常处理;对于钩子字段,定义对应的 nb_<字段名>(self, instance) 即可。

  • 优势:更模块化、更易维护、减少对底层逻辑的侵入。