1. 序列化器到底在帮我们解决什么问题先聊一个实际场景。前后端分离的项目里前端要的是 JSON后端操作的是 Django 的 QuerySet 和模型对象这两者之间隔着一条河。DRF 里的 Serializer 就是过河的桥它负责把 Python 对象翻译成 JSON 格式返回给前端也负责把前端提交的 JSON 转回 Python 对象和模型实例。很多刚接触 DRF 的朋友会把 Serializer 简单理解成“把一个 queryset 变成字典的工具”但它的价值远不止这一层。序列化器真正帮你包揽了三件事第一对象到原始数据字典/JSON的转换这一步叫序列化第二原始数据到对象的转换这一步叫反序列化第三数据合法性校验也就是你提交上来的字段是否符合模型约束和业务规则。为什么不直接用json.dumps()搞定原因很简单Django 的 QuerySet、Datetime 对象、Decimal 金额、UUID 这些数据类型json.dumps()默认压根不认识。你当然可以一个个手动转换但一旦字段多了、关系嵌套了代码会膨胀到没法维护。我在一个订单项目里就试过手写 dict 构造响应数据一个订单关联用户、商品、物流三张表嵌套两层之后光组装数据就写了三十几行后来全部换成了 Serializer代码量缩了一半还多而且结构清晰得多。这个知识点系列是围绕 DRF 核心组件逐个拆解的第一篇先聊 Serializer 全用法适合刚入门 DRF 的人也适合已经用了很久但有些细节没彻底搞明白的人。接下来我会从定义方式、字段映射、校验流程、模型序列化器、关联字段处理、视图配合使用、性能优化几个维度逐个展开每个环节都带实际代码和踩坑经验。2. 定义 Serializer先把字段映射玩明白2.1 基础定义方式与字段类型对照Serializer 的定义方式很直观直接继承serializers.Serializer类属性就是一个个字段对象字段类型决定了数据的转换规则和校验规则。from rest_framework import serializers class UserSerializer(serializers.Serializer): id serializers.IntegerField(read_onlyTrue) username serializers.CharField(max_length150) email serializers.EmailField() birth_date serializers.DateField(requiredFalse, allow_nullTrue)字段类型和 Django 模型字段的对应关系大概是这样的Serializer 字段对应模型字段说明CharFieldCharField / TextField字符串可限制 max_length / min_lengthIntegerFieldIntegerField整数可限制 max_value / min_valueFloatFieldFloatField浮点数DecimalFieldDecimalField精确小数必须指定 max_digits / decimal_placesBooleanFieldBooleanField布尔值DateFieldDateField日期DateTimeFieldDateTimeField日期时间常用 format 参数EmailFieldEmailField邮箱格式校验UUIDFieldUUIDFieldUUID 格式校验JSONFieldJSONField字典或列表ListField / DictFieldJSONField嵌套列表或字典可配 child 参数有个细节容易被忽略IntegerField接收的字符串数字会自动转成 intDecimalField会自动转成 Decimal 类型再校验精度。这意味着你需要清楚validated_data里的数据类型尤其是 Decimal 不能直接和 float 比较否则容易踩精度坑。2.2 read_only 和 write_only 的设置哲学这两个参数是 Serializer 里最常用也最容易搞混的一对。read_onlyTrue表示这个字段只在序列化输出时出现客户端提交数据时不会被接受。典型场景就是id、created_at、updated_at这类由系统自动生成的字段。你不希望用户伪造一个 id 来覆盖已有记录所以输出可以输入不行。write_onlyTrue正好相反只允许客户端提交序列化返回时不输出。最典型的就是密码字段。我之前做过一个用户接口如果密码不做 write_only 设置序列化时会把密码的哈希值一起返回给前端。虽然哈希值不是明文但也属于敏感信息泄露直接write_onlyTrue就把这个问题从根上解决了。还有一类字段比如用户确认密码confirm_password它本来就不存在于模型上只用于校验所以必须write_onlyTrue而且通常还要配合自定义validate()来和password字段比对。2.3 常用参数required、allow_null、default、source先看一个综合示例class ProductSerializer(serializers.Serializer): id serializers.IntegerField(read_onlyTrue) name serializers.CharField(max_length200) price serializers.DecimalField(max_digits10, decimal_places2) status serializers.ChoiceField(choices[on, off], defaulton) remark serializers.CharField(requiredFalse, allow_blankTrue, default)requiredFalse允许客户端不传这个字段。allow_nullTrue允许前端传null和required是两码事。requiredFalse是字段可以缺失allow_nullTrue是字段值可以是None别搞混。有人被这个坑过字段没传时觉得没事传了 null 反而报错就是因为只设置了requiredFalse没设allow_null。default字段缺失时使用默认值。注意default只在反序列化时生效。source这个参数极其有用它指定字段值从对象或数据的哪个属性获取。比如前端传了user_name模型字段叫username你可以这样写username serializers.CharField(sourceuser.name)source还能点号穿透访问属性做自定义展示时特别好用。我经常用它把模型的get_full_name()方法或status_display这类属性直接暴露给接口而不需要重写to_representation。3. 校验流程与反序列化这是重点中的重点3.1 is_valid() 背后发生了什么当你在视图中执行serializer.is_valid()时DRF 会按顺序做这几件事对每个字段执行to_internal_value()把原始数据转换成 Python 类型同时执行字段内置的校验比如类型、长度、范围。如果字段定义了validators列表按顺序执行这些校验器。如果 Serializer 中定义了validate_字段名方法会单独对这个字段做校验。最后执行整个 Serializer 级别的validate()方法所有字段校验通过后统一做对象级校验。校验通过后的数据放在serializer.validated_data里它是一个有序字典失败的原因放在serializer.errors里同样是一个字典key 是字段名value 是错误信息列表。serializer UserCreateSerializer(datarequest.data) if serializer.is_valid(): clean_data serializer.validated_data # 使用清洗后的数据创建用户 User.objects.create(**clean_data) else: return Response(serializer.errors, status400)这里有个十分常见的错误认知以为is_valid()执行完validated_data里的字段就一定可以直接传给Model.objects.create()。其实不一定比如confirm_password这个虚拟字段就不在模型里直接用**clean_data会直接报TypeError。正确做法是先 pop 掉这些额外字段。3.2 字段级校验和对象级校验的区别字段级校验用validate_字段名方法只关心单个字段def validate_username(self, value): if User.objects.filter(usernamevalue).exists(): raise serializers.ValidationError(用户名已被占用) return value注意两点。第一校验方法必须返回清洗后的值不返回的话这个字段在validated_data里就会丢第二命名是validate_加字段名还是validate_字段名是validate_username中间没有双下划线之外的符号很多新手会写错成validate_user_name或者直接重写validate_field。对象级校验用validate方法能拿到所有字段的清洗结果适合做跨字段判断def validate(self, attrs): password attrs.get(password) confirm_password attrs.get(confirm_password) if password and confirm_password and password ! confirm_password: raise serializers.ValidationError({confirm_password: 两次密码不一致}) return attrs3.3 自定义校验器函数式和类式校验器有两种写法一种是在字段定义时传入validators[...]列表def validate_phone(value): if not re.match(r^1[3-9]\d{9}$, value): raise serializers.ValidationError(手机号格式不正确) return value class UserSerializer(serializers.Serializer): phone serializers.CharField(validators[validate_phone])另一种是 DRF 提供的类式校验器比如UniqueValidator用于唯一性校验from rest_framework.validators import UniqueValidator class UserSerializer(serializers.Serializer): username serializers.CharField( validators[UniqueValidator(querysetUser.objects.all(), message用户名已存在)] )类式校验器适合需要携带配置的情况。UniqueValidator内部实现是查库判断性能上要注意如果接口高频调用可以把校验放到数据库约束层面性能更好。3.4 partialTrue 的部分更新PATCH 请求做部分更新时必须传partialTrue。比如前端只传email一个字段不传username如果没加partialTrueDRF 会因为username缺少而报 400。serializer UserSerializer(instanceuser, datarequest.data, partialTrue)这里有个容易忽略的细节当partialTrue时字段校验会跳过缺失字段但已经传入的字段该校验还会校验。所以你不必担心部分更新会绕过数据规则。4. ModelSerializer 开发效率直接翻倍4.1 Meta 配置的三种常用姿势实际项目里很少直接继承Serializer基本都是用ModelSerializer它最大的价值是能根据模型自动生成字段和校验规则class ArticleSerializer(serializers.ModelSerializer): class Meta: model Article fields __all__fields __all__会把模型所有字段都映射进来开发阶段挺方便但上线后我建议还是显式列出字段名列表一方面避免模型新增字段时接口响应意外变化另一方面字段顺序可控。如果你想排除某几个字段可以用excludeclass Meta: model Article exclude (content, internal_status)fields和exclude二选一千万别同时指定否则直接报错。4.2 extra_kwargs 调整字段参数模型字段映射到 Serializer 字段时默认参数不一定满足业务需求。比如模型里created_at是自动生成的但 ModelSerializer 默认把它映射为可写的read_onlyFalse这会导致用户在创建时提交伪造的创建时间。解决办法是用extra_kwargs调整class ArticleSerializer(serializers.ModelSerializer): class Meta: model Article fields (id, title, content, author, created_at) extra_kwargs { created_at: {read_only: True}, content: {required: False, allow_blank: True}, }这种写法比在类内部重新声明一个字段要简洁得多。当然如果你需要重新指定字段类型比如把一个 CharField 改成 JSONField那直接在类里显式声明字段也可以显式声明的字段优先级高于 Meta 自动生成的映射。4.3 何时需要自定义 create 和 updateModelSerializer 默认的create就是Model.objects.create(**validated_data)update就是逐个字段setattr后save()。单独使用没问题一旦遇到嵌套数据就会出问题。比如创建订单时同时要创建关联的订单明细前端传了这样的数据{ order_no: 202401010001, items: [ {product_id: 1, quantity: 2}, {product_id: 3, quantity: 1} ] }默认create拿到items这个列表后直接传给Order.objects.create一定会报TypeError因为 Order 模型没有items字段。这时候必须自定义createclass OrderSerializer(serializers.ModelSerializer): items OrderItemSerializer(manyTrue, write_onlyTrue) class Meta: model Order fields (id, order_no, items) def create(self, validated_data): items_data validated_data.pop(items) order Order.objects.create(**validated_data) for item_data in items_data: OrderItem.objects.create(orderorder, **item_data) return order同理update也有类似的场景比如替换整个关系列表需要先删除旧关联再创建新关联。这些操作放 Serializer 层比放 view 层更合适因为 Serializer 自己知道数据是怎么被清洗出来的视图层只需要调用save()就行。5. 序列化关联字段处理外键和多对多5.1 嵌套序列化最直观但最容易产生性能坑关联字段最自然的写法是嵌套一个子 Serializerclass CategorySerializer(serializers.ModelSerializer): class Meta: model Category fields (id, name) class ProductSerializer(serializers.ModelSerializer): category CategorySerializer(read_onlyTrue) class Meta: model Product fields (id, name, price, category)这种写法输出结构清晰前端拿到的是完整的嵌套对象。但代价是如果你查询了 100 个商品每个商品都要额外查一次 Category 表这就是著名的 N1 查询问题。优化方式是在视图层用select_related(category)关联查询一次 join 解决序列化器代码不用改。5.2 四类常用关联字段的选择嵌套序列化不是唯一方案。实际开发中我常用的还有这几种字段类输出示例适用场景PrimaryKeyRelatedField3只需要 ID前端拿 ID 再去查详情SlugRelatedField电子设备用某个唯一字段标识关联对象StringRelatedField张三直接展示模型的str结果SerializerMethodField任意完全自定义输出逻辑PrimaryKeyRelatedField是最轻量的方案也是我最常用的。定义时注意两点创建场景下必须配queryset参数用于校验传入的 ID 是否存在读取场景下可以直接设为read_onlyTrue。category serializers.PrimaryKeyRelatedField(querysetCategory.objects.all())SerializerMethodField是万金油灵活性最高。比如返回商品的分类名称、库存状态等组合信息class ProductSerializer(serializers.ModelSerializer): category_name serializers.SerializerMethodField() status_display serializers.SerializerMethodField() class Meta: model Product fields (id, name, price, category_name, status_display) def get_category_name(self, obj): return obj.category.name if obj.category else 未分类 def get_status_display(self, obj): return obj.get_status_display()用SerializerMethodField时有个小习惯方法名必须是get_字段名字段名和方法名对不上会直接报AttributeError。这个字段在反序列化时天然是只读的不需要也不能传值进来。5.3 超链接序列化器 HyperlinkedModelSerializerDRF 还提供了一种把关联关系输出成 URL 的形式HyperlinkedModelSerializer。它自动把主键字段替换成url字段指向资源详情地址class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model User fields (url, id, username, email)这种方案适合纯 API 场景客户端通过链接导航资源符合 REST 风格。但说实话我在实际前后端分离项目里很少用因为前端更倾向于直接拿 ID 自己调接口超链接反而增加了解析成本。如果你在做 HATEOAS 风格接口或者给第三方开放 API可以考虑用。6. 与视图协同工作串联整套请求链路6.1 APIView 中的标准用法Serializer 单独拿出来只是一堆代码真正让它发挥作用的是和视图配合。在APIView里最标准的一套流程是这样from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status class UserListView(APIView): def get(self, request): users User.objects.all() serializer UserSerializer(users, manyTrue) return Response(serializer.data) def post(self, request): serializer UserCreateSerializer(datarequest.data) if serializer.is_valid(): user serializer.save() return Response(UserSerializer(user).data, statusstatus.HTTP_201_CREATED) return Response(serializer.errors, statusstatus.HTTP_400_BAD_REQUEST)manyTrue表示序列化的是一个对象列表不加这个参数去序列化 QuerySet 会直接报错。你传入单个对象时用serializer.data前端收到的是字典传入manyTrue时收到的是数组。serializer.save()是个语法糖它会自动根据是否传入了instance来决定调用create()还是update()。传入instance并调用save()就是更新只传data调用save()就是创建。6.2 context 传递请求数据很多场景下 Serializer 内部需要访问当前登录用户或者请求对象。比如创建文章时自动把request.user设为作者而不需要前端传author_id。# view 中 serializer ArticleSerializer(datarequest.data, context{request: request}) # serializer 中 class ArticleSerializer(serializers.ModelSerializer): class Meta: model Article fields (id, title, content, author) def create(self, validated_data): validated_data[author] self.context[request].user return super().create(validated_data)context是一个普通字典你在视图里放什么Serializer 里就能通过self.context.get(key)取到什么。基础写法要记住因为后面做自定义权限校验、动态控制字段可见性都会用到。6.3 重写 to_representation 定制输出结构有些时候前端需要的结构跟模型结构差异很大比如枚举值想转成中文时间格式想自定义或者想统一包裹一层数据。这时候可以重写to_representation方法class OrderSerializer(serializers.ModelSerializer): class Meta: model Order fields (id, order_no, status, created_at) def to_representation(self, instance): data super().to_representation(instance) data[status_text] instance.get_status_display() data[created_at] instance.created_at.strftime(%Y-%m-%d %H:%M:%S) return data这个方法的执行时机是序列化输出阶段只影响返回结构不影响反序列化。所以如果你只是想在输出时增加一些字段用SerializerMethodField就够如果想对已有输出做统一加工重写to_representation更合适。有个实际场景我遇到过多次列表页只需要简化信息详情页需要完整信息。一种做法是两个 Serializer 各写一套另一种做法是在同一个 Serializer 里判断self.context.get(detail)是否为真动态决定输出哪些字段。后者代码维护起来更方便因为校验逻辑公用。7. 性能优化与常见坑排查实录7.1 N1 查询问题的优化嵌套序列化带来的最大隐患就是 N1 查询。列表接口返回 50 条数据每条都查关联表总共就是 51 条 SQL数据库压力直接上去。解决办法是视图层用 ORM 预取# 外键关系用 select_related users User.objects.select_related(profile).all() # 多对多或反向外键用 prefetch_related articles Article.objects.prefetch_related(tags).all()预取之后查询次数恒定为 2 次左右性能提升非常明显。我在一个订单列表优化里接口响应时间从 1.8 秒降到了 0.3 秒只加了两个select_related。7.2 大数据量序列化的替代方案Serializer 虽然好用但它的主要成本是对象到字典的转换。当你只需要返回纯粹的数据列表、不需要嵌套转换、不需要数据校验时直接用 ORM 的values()反而更快data list(Product.objects.values(id, name, price)) return Response(data)这种方式绕过了 Serializer 的全部逻辑性能最高。缺点是没有字段类型转换比如 DateField 不会被规范成 ISO 格式Decimal 也不会转成字符串所以返回前要确保前端能正确解析。我的经验是内部管理后台列表页用values()足够对外 API 还是走 Serializer因为一致性更重要。7.3 DateTimeField 时区与格式的坑DRF 默认的DateTimeField输出格式是 ISO 8601 格式也就是2025-01-01T10:00:00.123456Z这种。前端如果不做处理直接展示会很难看。你可以全局配置默认格式REST_FRAMEWORK { DATETIME_FORMAT: %Y-%m-%d %H:%M:%S, }也可以在单个字段上指定created_at serializers.DateTimeField(format%Y-%m-%d %H:%M:%S, read_onlyTrue)另外注意时区问题。如果项目设置了USE_TZTrue存储的是 UTC 时间输出时要想清楚到底返回 UTC 还是本地时间。DRF 默认会用CURRENT_TIMEZONE做转换如果你不做任何配置前端拿到的可能是 UTC 时间和本地时间差 8 个小时排查起来很头疼。建议在项目一开始就明确时区规范前端统一用时间戳后端统一输出带时区的 ISO 字符串避免格式混用。7.4 常见问题速查表问题现象大概率原因解决方案字段不传时报 400传 null 也报 400required 和 allow_null 未同时配置按需求加requiredFalse, allow_nullTrue序列化输出没有某些字段字段被定义为 write_only检查字段是否设置为write_onlyTrue创建时报 TypeError: got an unexpected keyword argumentvalidated_data 里有虚拟字段或嵌套字段自定义 create 方法中 pop 掉额外字段PATCH 请求校验失败缺少部分必填字段实例化时传partialTrue列表接口超慢嵌套序列化导致 N1 查询视图加select_related/prefetch_related外键传入 ID 时校验错误PrimaryKeyRelatedField 未配置 queryset加上queryset相关模型.objects.all()自定义校验方法没生效方法命名错误不是validate_字段名检查方法名确保与字段名一致序列化器修改后接口结构变化模型新增字段被自动带出fields 显式列出所有字段提示排查序列化问题最有效的手段是打印serializer.errors。很多新手一看到 400 就蒙了其实把 errors 打出来DRF 已经把具体校验失败原因写得非常清楚了。7.5 我踩过的几个不太容易发现的坑第一个是Serializer.data的缓存问题。同一个 Serializer 实例如果你修改了底层对象的数据再次访问.data拿到的还是旧缓存。解决办法很简单重新实例化 Serializer 或者调用.data前确保对象已保存并重新查询。第二个是DecimalField的精度。DRF 默认把 Decimal 序列化为字符串而不是数字这是为了防止 JavaScript 浮点精度丢失。如果前端非要用数字类型你可以在字段上设置coerce_to_stringFalse但要注意大金额或高精度场景下的精度风险。第三个是ChoiceField的显示值。模型里定义了choices后序列化默认输出的是数据库存的on、off这类短代码而前端往往需要显示中文。推荐用SerializerMethodField调用 Django 的get_字段名_display()方法输出人类可读的文本。写在最后序列化器是 DRF 中最基础也最关键的一块花时间把它彻底搞透后面的视图、视图集、权限、分页这些东西用起来都会顺很多。我自己从最早手写 dict、到用 Serializer、再到会用 ModelSerializer 优化开发效率、最后学会针对特殊场景重写方法这个过程大概花了两三个实际项目才真正熟练。如果你刚开始学 DRF我建议别急着去看 ViewSet 和 Router先把 Serializer 的序列化、反序列化、校验这三件事的流程理清楚然后拿一个真实的小模型练手写一个接口返回嵌套数据再写一个创建接口处理关联字段把自定义校验加上。这套流程跑通了DRF 就算是入门了。下一篇我会接着写视图和视图集的知识点到时候见。