DRF序列化器完全指南:从字段到验证,彻底搞懂Serializer
同期做Django后端的朋友十有八九是在前后端分离的接口项目里被Serializer折磨过的。说折磨不是因为它难而是网上太多教程把序列化器拆得稀碎一会儿讲Serializer一会儿讲ModelSerializer字段参数一堆验证钩子好几个真到自己写接口时反而不知道怎么组合。这篇我把DRF的Serializer从头到尾捋一遍用最直白的说法把序列化、反序列化、字段声明、验证链路、保存逻辑全部串起来零基础也能顺着走一遍已经在写接口的也能当手册查漏补缺。1. 序列化器到底解决了什么问题——先弄懂它的存在价值1.1 前后端分离模式下的数据格式转换难题在没有DRF的远古时期Django开发者做Web开发最常见的方式是服务端渲染视图函数从数据库取出一堆模型对象塞进模板然后渲染成完整的HTML页面返回给浏览器。那时候根本没有Serializer这个概念因为数据从模型到页面的转换是由模板语言干的。前后端分离之后后端不再负责渲染页面只负责提供数据接口前端用Ajax或fetch请求接口拿到JSON再由JavaScript渲染页面。这时候问题就来了数据库里存的、ORM查出来的都是QuerySet和模型对象而HTTP接口传输的标准格式是JSON中间必须有一个翻译的过程。你当然可以手动做# 视图里手动做代码又丑又散 data { id: book.id, title: book.title, price: str(book.price), pub_date: book.pub_date.isoformat(), } return JsonResponse(data)一个模型这么干忍忍也就算了项目里有二三十张表、上百个字段的时候手写转换就是灾难每个视图都要复制粘贴一套字段映射加字段要改到处找字段类型转JSON的格式不统一嵌套关联的模型更是无从下手。Serializer就是专门来解决这个模型对象-JSON双向转换问题的而且它还顺手把参数校验、数据落库这两件事一起打包了。1.2 Serializer、ModelSerializer 与 Django Form 的家族关系DRF的Serializer在设计上大量借鉴了Django自带Form的思路你可以把它理解成API版本的Form。Django的Form负责把前端提交的表单数据清洗成Python数据再做校验Serializer负责把前端提交的JSON数据清洗成Python数据类型再做校验。两者核心套路一致声明字段、校验、拿清洗后的数据。但两者有个关键差异Form的终点是帮你把表单render成HTML控件Serializer的终点是帮你把模型对象转成JSON以及把提交的JSON还原成模型对象的修改。所以Serializer多了序列化方向对象转JSON的能力这是Form没有的。至于Serializer和ModelSerializer的关系就更简单了。Serializer是手写版每个字段都要自己声明灵活但费劲ModelSerializer是自动版你告诉它对应哪个模型它就根据模型的字段定义自动生成Serializer字段还能自动生成create和update的实现代码。实际项目中90%的场景用ModelSerializer足够但理解Serializer的底层逻辑才是用好ModelSerializer的前提不然你根本不知道Meta里那些配置到底在背后做了什么。2. Serializer基础用法从手动JSON转换到声明式API2.1 定义一个最小可用的Serializer以一个图书管理场景为例假设有这样两个模型# models.py from django.db import models class Author(models.Model): name models.CharField(max_length50) email models.EmailField() def __str__(self): return self.name class Book(models.Model): title models.CharField(max_length100) price models.DecimalField(max_digits5, decimal_places2) pub_date models.DateField() author models.ForeignKey(Author, on_deletemodels.CASCADE, related_namebooks)用Serializer手写对应接口的数据格式定义长这样# serializers.py from rest_framework import serializers class BookSerializer(serializers.Serializer): id serializers.IntegerField(read_onlyTrue) title serializers.CharField(max_length100) price serializers.DecimalField(max_digits5, decimal_places2) pub_date serializers.DateField() author_name serializers.CharField(sourceauthor.name, read_onlyTrue)第7行的source参数值得先提一嘴它表示当前字段值不是直接从模型对象的同名属性取的而是从instance.author.name这个位置取的。后面序列化、反序列化都会用到这个机制是处理字段名不一致问题的常用手段。2.2 序列化过程模型对象如何变成JSON序列化方向就是把模型实例或QuerySet变成Python原生数据类型再交给Response进行JSON渲染。用法非常固定# 单个对象 book Book.objects.select_related(author).get(pk1) serializer BookSerializer(book) data serializer.data # data OrderedDict([(id, 1), (title, ...), (price, ...), (pub_date, ...), (author_name, ...)]) # 多个对象 books Book.objects.select_related(author).all() serializer BookSerializer(books, manyTrue) data serializer.data # data list of OrderedDict执行serializer.data时Serializer会遍历声明的每个字段调用字段的to_representation方法把Python对象里的值转换成适合JSON序列化的基本类型。比如DecimalField会把Decimal转成字符串DateField把date对象转成2025-06-01格式的字符串。这里有个新手经常晕的点serializer.data拿到的不是JSON字符串而是Python内置的数据结构dict、listJSON字符串的转换是在Response里发生的。所以你在视图里直接用serializer.data传给任何需要dict/list的地方都没问题不用反复json.dumps。2.3 反序列化过程JSON如何变回模型对象反序列化是数据进来的方向负责把前端传来的JSON转成Python数据类型并做校验from rest_framework import serializers data { title: Django实战, price: 58.00, pub_date: 2025-06-01, } serializer BookSerializer(datadata) if serializer.is_valid(): validated_data serializer.validated_data # 这里拿到的就是清洗后的Python原生数据 print(validated_data) else: print(serializer.errors)传datadata到Serializer构造器时这个Serializer就进入反序列化模式。is_valid()会依次执行字段类型转换、字段级验证、对象级验证所有步骤通过后清洗过的数据存在validated_data里任何一步失败错误信息进errors字典。我刚开始用DRF时总有个误区以为调is_valid()之前Serializer就有validated_data。实际上validated_data在is_valid()之前根本不存在直接访问会抛AssertionError。判断Serializer处于序列化还是反序列化模式就看它是Serializer(instance)还是Serializer(data...)构造的这两个模式不能混用。如果你又传instance又传data那会进入更新已有对象的模式后面讲到create和update时再说。3. ModelSerializer自动生成的捷径与手动控制的边界3.1 自动字段生成与默认行为实际开发中手写Serializer声明所有字段实在太累了ModelSerializer才是主力class BookSerializer(serializers.ModelSerializer): class Meta: model Book fields [id, title, price, pub_date, author]就这几行ModelSerializer会自动读取Book._meta.fields按模型字段类型映射成对应的Serializer字段CharField变CharField、DecimalField变DecimalField、ForeignKey变PrimaryKeyRelatedField序列化时输出主键值、DateField变DateField以此类推。对照一下模型和序列化器会发现几个默认行为非常关键模型字段定义ModelSerializer自动生成的字段默认read_onlyAutoField主键IntegerField是普通字段CharField等对应类型的Serializer字段否ForeignKeyPrimaryKeyRelatedField否带editableFalse的字段对应字段是带auto_nowTrue的DateTimeField对应字段是这个表格值得记一下数据库自动管理时的字段主键、auto_now时间戳在ModelSerializer里默认是只读的前端传什么都会被忽略更新或创建时DRF也不会拿提交的值去覆盖。这避免了很多前端手贱传了个id把数据库改了的事故。3.2 用Meta类配置字段裁剪、只读与校验ModelSerializer的Meta类里能干的活很多最常用的是这几个配置class BookSerializer(serializers.ModelSerializer): class Meta: model Book fields [id, title, price, author] read_only_fields [id, author] extra_kwargs { title: {max_length: 50, error_messages: {max_length: 标题不能超过50个字}}, price: {min_value: 0}, }fields控制要暴露哪些字段。也可以写成__all__表示全量输出但我不推荐接口字段一旦暴露就很难收敛写明确清单更利于维护。read_only_fields这些字段只输出不接收前端传入。注意不能把Meta里手动声明的字段放进read_only_fields那是给自动生成字段用的。手动声明的字段直接加read_onlyTrue参数。extra_kwargs针对自动生成的字段做额外参数补充相当于不重写字段的前提下微调行为比如覆盖错误提示、约束值范围、设置required等。还有exclude可以排除某些字段但同样不推荐理由和__all__一样——显式优于隐式。3.3 什么时候不该用ModelSerializerModelSerializer虽然省事但有些场景反而碍手碍脚嵌套深度变了比如序列化图书时希望返回author的完整信息姓名、邮箱而不是只返回author_id。默认生成的是PrimaryKeyRelatedField输出1这种主键没意义。此时要么重写author字段为一个嵌套的AuthorSerializer要么自己手写Serializer。接口的数据结构和模型完全不同比如一个统计接口返回本月销量已售罄数量这些没有对应模型字段与其硬套ModelSerializer不如直接用Serializer。需要平铺或重命名字段比如前端要author_name模型里叫author.nameModelSerializer不重写字段是做不到这个映射的。一句话模型结构和接口结构接近时优先ModelSerializer差得远时果断用Serializer手写别硬凑。中大型项目经常是两者混用一个接口里自定义字段和ModelSerializer共同存在。4. 字段与参数数据进出API的最后一道闸门4.1 常用字段类型与选型对照Serializer提供的字段类型很多但日常高频使用的基本就这几个字段类型对应前端/JSON类型常见模型对应关系说明IntegerFieldnumberIntegerField、AutoField整型FloatFieldnumberFloatField浮点型注意精度问题DecimalFieldstringDecimalField转成字符串传输避免精度丢失CharFieldstringCharField、TextField字符串可限制max_length / min_lengthBooleanFieldbooleanBooleanField布尔值DateField / DateTimeFieldstringDateField / DateTimeField默认格式YYYY-MM-DD HH:MM:SSEmailFieldstringEmailField自带邮箱格式校验URLFieldstringURLField自带URL格式校验FileField / ImageFieldstring(URL)FileField / ImageField文件上传场景PrimaryKeyRelatedFieldnumberForeignKey关联对象的主键表示SerializerMethodField任意无直接对应自定义方法返回值SerializerMethodField特别说一下它是只读计算字段的代表工具。比如给Book加一个discount_price字段逻辑是价格打八折class BookSerializer(serializers.ModelSerializer): discount_price serializers.SerializerMethodField() class Meta: model Book fields [id, title, price, discount_price] def get_discount_price(self, obj): return round(float(obj.price) * 0.8, 2)方法名有固定格式get_字段名接收的obj就是当前序列化的模型实例。这种字段天然read_only只用于输出不接收传入。4.2 关键参数详解required、read_only、write_only字段参数是Serializer里最容易让人混的地方尤其是这几个requiredTrue默认反序列化时前端必须传这个字段否则is_valid()直接失败。如果接口设计某些字段可选要设requiredFalse。read_onlyTrue该字段只用于序列化输出反序列化时被忽略。适合主键、创建时间这类由服务端决定的字段。write_onlyTrue该字段只用于反序列化输入序列化时不输出。最典型的场景就是注册接口的密码字段另一个场景是创建资源时接收一个外键id但输出时只展示嵌套详情所以把外键id设成write_only。allow_nullTrue允许前端传null。注意requiredFalse和allow_nullTrue是两回事前者是前端可以不传这个键后者是传null时能通过校验。只设requiredFalse但没设allow_null前端传了null照样报错。default...不传时使用默认值。有默认值的字段相当于隐式requiredFalse。validators[]给字段挂额外的验证函数列表。这三组参数必须区分清楚我在团队的代码评审里见过无数次把requiredFalse当allow_null用的Bug。4.3 自定义字段与字段级重写如果内置字段满足不了需求可以继承某个基础字段重写to_representation和to_internal_value两个方法class CommaSeparatedTagsField(serializers.Field): 把模型里的字符串 a,b,c 转成接口用的列表 [a,b,c] def to_representation(self, value): return value.split(,) if value else [] def to_internal_value(self, data): if not isinstance(data, list): raise serializers.ValidationError(tags必须是一个列表) return ,.join(data)to_representation从模型对象取值 - 转成序列化输出值。to_internal_value从传入数据取值 - 转成Python内部值同时负责校验。有了这个基础任何数据库存的是A格式、接口要的是B格式的字段都能处理。5. 验证链路全拆解执行顺序与验证器选择5.1 字段级验证validate_字段名Serializer的验证机制走的是一整套钩子流程。每个字段在进入validated_data之前会依次经过内置类型转换、字段级验证、自定义验证器、对象级验证四层关卡。字段级验证方法写法固定class BookSerializer(serializers.Serializer): title serializers.CharField(max_length100) pub_date serializers.DateField() def validate_price(self, value): if value 0: raise serializers.ValidationError(价格不能为负数) if value 1000: raise serializers.ValidationError(价格超出合理范围) return value注意第4行的validate_price方法名是validate_加字段名。它接收的value是已经通过内置字段转换的Python类型比如price字段用DecimalField声明这里收到的已经是Decimal对象不是字符串。方法执行完必须返回清洗后的值这个值会被写进validated_data。如果校验失败抛出的ValidationError会被DRF捕获并转成400响应里的错误结构。5.2 对象级验证validate方法对象级验证适合需要同时看多个字段的场景比如注册接口要校验两次密码是否一致class RegisterSerializer(serializers.Serializer): password serializers.CharField(write_onlyTrue) confirm_password serializers.CharField(write_onlyTrue) def validate(self, attrs): if attrs[password] ! attrs[confirm_password]: raise serializers.ValidationError(两次输入的密码不一致) attrs.pop(confirm_password) return attrs这里的入参attrs是整个字段校验完成后的所有数据组成的字典比字段级验证晚一步执行。对象级验证里放跨字段的判断逻辑最合适单个字段的取值范围检查放字段级更清晰。5.3 验证器与整体执行顺序除了方法式验证DRF还支持函数式验证器from rest_framework import serializers def validate_title_start(value): if not value.startswith([精选]): raise serializers.ValidationError(书名必须以[精选]开头) return value class BookSerializer(serializers.Serializer): title serializers.CharField(max_length100, validators[validate_title_start])把一个可调用对象塞进字段的validators列表即可。DRF内置的UniqueValidator、UniqueTogetherValidator也很常用前者配合queryset参数检查唯一性后者检查联合唯一约束。一个Serializer的is_valid()到底按什么顺序执行我整理一份执行链路对每个字段运行类型转换to_internal_value如果类型对不上直接记录该字段错误。对类型转换成功的字段运行字段内置校验如max_length、min_value。运行字段名对应的validate_字段名方法。运行字段的validators列表中的验证器。全部字段通过后调用对象级validate(attrs)方法。这个顺序记牢了排查校验问题会快很多。比如一个字段既转换失败又触发自定义验证器最终你看到的错误只是转换失败的提示因为到第三步之前就被拦住了。5.4 is_valid传入raise_exception的小技巧视图里常见的写法是serializer.is_valid(raise_exceptionTrue)这实际上是一个DRF提供的便捷开关一旦校验失败会直接抛ValidationError被DRF异常处理转成400响应错误结构自动按字段组织。如果你不传这个参数就必须自己判断返回400包裹errors代码会重复很多。写DRF接口我的习惯是一律用raise_exceptionTrue控制流交给框架处理视图里只写校验通过之后的逻辑。6. create与update反序列化之后的数据落库逻辑6.1 默认实现逻辑剖析反序列化的终点是拿到validated_data但要真正写入数据库必须调用serializer.save()。Save内部做了什么它检查self.instance是否存在如果构造时传了instance走update(instance, validated_data)。如果没有传instance走create(validated_data)。Serializer本身的create和update不实现任何逻辑直接抛NotImplementedError。ModelSerializer则默认实现了它们# ModelSerializer内置逻辑简化版 def create(self, validated_data): return self.Meta.model.objects.create(**validated_data) def update(self, instance, validated_data): for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() return instance这里有一个坑ModelSerializer默认的create/update只处理简单字段一碰到嵌套写入比如创建一本书同时传author的完整数据就会炸。因为validated_data里的author不做处理直接塞给objects.createORM会报错。处理嵌套数据通常有两种方式一种是视图层先把外键id查成实例放进validated_data另一种是重写create/update方法。方式一更少侵入推荐优先考虑# 视图里处理 serializer BookSerializer(datarequest.data) if serializer.is_valid(raise_exceptionTrue): author_id request.data.get(author_id) author Author.objects.get(pkauthor_id) serializer.save(authorauthor) # 通过save传值会把author合并进validated_data注意save(authorauthor)这个写法位置参数必须对应Serializer里的字段名。这个技巧在创建操作需要注入当前登录用户时极其常用# APIView里 serializer.save(authorself.request.user)6.2 重写create处理单层外键关系如果接口层天然要接收author_id不想在视图写两行查询重写to_internal_value或create都行。最常见的写法是在Serializer里重写createclass BookCreateSerializer(serializers.ModelSerializer): author serializers.PrimaryKeyRelatedField(querysetAuthor.objects.all()) class Meta: model Book fields [id, title, price, pub_date, author] read_only_fields [id] def create(self, validated_data): return Book.objects.create(**validated_data)这里的PrimaryKeyRelatedField承担了一个关键作用它负责把前端传来的author_id整型数据转换成Author模型实例转换完的validated_data[author]已经是对象ModelSerializer默认的create能直接消费。所以很多时候不是 ModelSerializer 的create不够用而是字段声明没选对。6.3 嵌套序列化器的写入模式真正复杂的是嵌套序列化器——比如创建一本新书时希望一次性提交作者的全部信息姓名、邮箱DRF支持用嵌套Serializer做读写class AuthorSerializer(serializers.ModelSerializer): class Meta: model Author fields [id, name, email] class BookNestedSerializer(serializers.ModelSerializer): author AuthorSerializer() class Meta: model Book fields [id, title, price, author] def create(self, validated_data): author_data validated_data.pop(author) author Author.objects.create(**author_data) return Book.objects.create(authorauthor, **validated_data)嵌套写入的处理必须重写create/update因为默认实现不会拆解嵌套字典。DRF官方文档提供了一套create/update嵌套数据的通用写法但记那一大段模板不如按实际项目拆开写逻辑更直观更易维护。嵌套层级超过两层时建议在方法里逐步拆解不要强行一个方法塞到底。6.4 关于save的一点重要说明save()方法会把validated_data和传入的keyword参数合并后再调create/update所以如果你想给创建对象附加一些不出现在Serializer字段里的属性比如当前登录用户、默认状态值直接save(userrequest.user)即可。但注意传入的keyword参数必须和字段名对得上否则DRF会报错。7. 实战经验汇总从字段声明到性能优化的避坑笔记7.1 踩过的坑read_only_fields与手动声明字段的冲突ModelSerializer的Meta.read_only_fields看起来很方便但如果你在Serializer里手动声明了一个同名字段read_only_fields对它完全不起作用。手动声明的字段要想只读必须自己在字段声明里写read_onlyTrue。这个坑我用过一次就长记性了因为在DRF源码里read_only_fields只作用于自动生成的字段手动字段会被当作自定义字段原样保留。7.2 序列化器的Null与默认值三条易错点CharField默认allow_blankFalse前端传空字符串会当作校验失败如果业务允许空字符串记得设allow_blankTrue。allow_nullTrue和defaultNone组合时DRF的行为稍有不同前者允许null输入且最终validated_data里是None后者会在不传时自动填入None。布尔字段有个隐性坑前端传false字符串时BooleanField会把它当True处理因为非空字符串被转成True。处理布尔值校验时最好在字段级validate里显式限定取值范围。7.3 序列化器性能select_related与prefetch_related序列化嵌套外键时最耗性能的操作就是N1查询。一个列表接口返回20本书每本书序列化时都去查作者信息算上列表本身总共发21次数据库查询。解决方式在视图层# 错误示范每本书序列化时单独查author books Book.objects.all() serializer BookSerializer(books, manyTrue) # 正确示范一次性把关联对象查出来 books Book.objects.select_related(author).all() serializer BookSerializer(books, manyTrue)select_related处理外键非常有用prefetch_related处理反向外键和ManyToMany比如AuthorSerializer里嵌套books列表时用authors Author.objects.prefetch_related(books).all() serializer AuthorDetailSerializer(authors, manyTrue)还有一个容易被忽视的性能点Serializer里如果用了SerializerMethodField方法里再做数据库查询N1问题依然存在且很难根治。我的建议是MethodField里尽量只做纯计算或读缓存的逻辑需要数据库查询的数据提前在视图里查好塞进对象缓存或上下文里。7.4 一个字段两套规则输入Serializer与输出Serializer分离很多新手尝试用同一个Serializer既处理创建又处理返回结果字段权限互相打架创建时需要write_onlyTrue的密码字段创建完返回时又需要输出用户信息一个Serializer往往顾此失彼。我的实践经验是一个模型可以有多个Serializer创建用CreateSerializer列表用ListSerializer详情用DetailSerializer返回给前端的Response serializer单独控制输出字段。这虽然多了几个类但每个Serializer只干一件事权限清晰也好扩展。class AuthorListSerializer(serializers.ModelSerializer): class Meta: model Author fields [id, name, book_count] class AuthorCreateSerializer(serializers.ModelSerializer): class Meta: model Author fields [id, name, email] read_only_fields [id]7.5 校验失败的错误信息结构DRF返回的错误信息默认是字段名到错误列表的映射如{title: [标题不能超过50个字]}。后端保存错误信息再返回给前端时不要直接用str(serializer.errors)会丢结构。正确做法是用serializer.errors原样返回DRF的Response会自动把它序列化成JSON结构前端能拿到结构化错误逐字段展示。如果要在日志里记录用json.dumps(serializer.errors, ensure_asciiFalse)保存完整信息。7.6 接口文档生成与Serializer的关系DRF的Schema生成依赖Serializer的字段声明字段声明得越规范自动生成的OpenAPI文档越完整。一个看似不起眼的help_text如果用上了文档里会有对应字段说明read_only、write_only配置对了文档里会出现正确的读写权限标识validators数组里的约束也会影响文档的参数格式。所以写Serializer时给字段补充help_text和注释不只是代码洁癖直接关系到文档质量和前端协作效率。我在实际项目里维护一个几十个Serializer的模块时最大的体会是Serializer的灵活性意味着规范全靠自觉。字段声明、校验逻辑、权限配置、性能优化每个环节都有可玩的空间但真正稳妥的做法是给团队定一套约定比如读接口统一用ListSerializer/DetailSerializer写接口统一用CreateSerializer/UpdateSerializer字段一律显式声明外键关系优先用read_only形式的嵌套输出。这套约定一旦定下来后面的人照葫芦画瓢项目质量基本不会跑偏。最后再说个我最近踩的小坑。因为接口需要兼容旧版字段名我起初想靠source参数在ModelSerializer里做映射结果发现source对写入方向的处理并不像想象中那么无脑在嵌套数据或反序列化视图里source指到的路径必须真实存在否则会静默失败或者直接抛KeyError。从那以后我对映射类需求一律显式重写字段或改用SerializerMethodField避免在source上花太多时间。毕竟序列化器这种基础组件稳定顺手比炫技重要得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →