第 13 章 · DRF Serializer 进阶与嵌套校验
本章目标:在 ch07 基础 ModelSerializer 之上,掌握 嵌套序列化、writable nested、SerializerMethodField、跨字段校验;实现列表/详情不同 Serializer;使用 ListSerializer 批量创建与 partial update;与 Flask ch13 Marshmallow 对照,在 shop-demo 商品 API 上完成进阶重构。
学时建议:4~5 小时(含 2 小时嵌套与批量实操)
前置:本模块 ch01~ch12;重点复习 ch05 Product/Category 模型、ch07 ProductListSerializer 与 ProductViewSet.get_serializer_class()。
13.1 从 ch07 基础到进阶
ch07 已用 ModelSerializer 完成商品列表与写入。接口增多后出现新需求:
| 需求 | ch07 做法 | ch13 进阶 |
|---|---|---|
| 详情含分类对象 | category_name = CharField(source=...) | 嵌套 CategorySerializer |
| 创建时同时建分类 | 前端两次请求 | writable nested |
| 列表要缩略图 URL | 视图里拼字符串 | SerializerMethodField |
| 价格与库存联动 | 分散在视图 | validate() 跨字段校验 |
| 批量导入 SKU | 循环 create | 自定义 ListSerializer |
| PATCH 只改部分字段 | partial_update 已支持 | 明确 partial=True 语义 |
Request JSON
→ Serializer(字段级 + 对象级校验)
→ validated_data
→ Model.save() / bulk_create
→ Serializer(嵌套/MethodField 输出)
→ Response JSON
教学项目统一使用 shop-demo、shopdemo、api.example.com、user-demo,不引用任何企业内部仓库或私有路径。
13.2 ModelSerializer 进阶配置
模型沿用 ch05 Product / Category(含 cover、description、外键 category)。catalog/serializers.py 进阶片段:
from decimal import Decimal
from rest_framework import serializers
from .models import Product, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name", "slug"]
read_only_fields = ["id"]
class CategoryWriteSerializer(serializers.ModelSerializer):
"""创建商品时内联新建分类(writable nested 简版)"""
class Meta:
model = Category
fields = ["name", "slug"]
def validate_slug(self, value):
if Category.objects.filter(slug=value).exists():
raise serializers.ValidationError("分类 slug 已存在")
return value
| Meta 选项 | 作用 |
|---|---|
fields / exclude | 白名单 / 黑名单(推荐白名单) |
read_only_fields | 输出只读,创建时忽略 |
extra_kwargs | 单字段 required/validators 覆盖 |
validators | 模型级唯一等校验 |
class ProductWriteSerializer(serializers.ModelSerializer):
category = CategoryWriteSerializer(required=False) # 嵌套写入
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(), source="category", write_only=True, required=False
)
class Meta:
model = Product
fields = [
"name", "slug", "price", "stock", "is_published",
"description", "category", "category_id",
]
def validate_price(self, value):
if value <= Decimal("0"):
raise serializers.ValidationError("价格必须大于 0")
return value
def validate(self, attrs):
"""跨字段:上架时库存必须 > 0"""
is_pub = attrs.get("is_published", getattr(self.instance, "is_published", False))
stock = attrs.get("stock", getattr(self.instance, "stock", 0))
if is_pub and stock <= 0:
raise serializers.ValidationError({"stock": "上架商品库存必须大于 0"})
return attrs
def create(self, validated_data):
category_data = validated_data.pop("category", None)
if category_data and "category" not in validated_data:
category, _ = Category.objects.get_or_create(
slug=category_data["slug"],
defaults={"name": category_data["name"]},
)
validated_data["category"] = category
return super().create(validated_data)
13.3 SerializerMethodField 与嵌套只读
class ProductListSerializer(serializers.ModelSerializer):
category = CategorySerializer(read_only=True)
cover_thumb = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
"id", "name", "slug", "price", "stock",
"is_published", "category", "cover_thumb", "created_at",
]
def get_cover_thumb(self, obj):
if not obj.cover:
return None
request = self.context.get("request")
url = obj.cover.url
return request.build_absolute_uri(url) if request else url
class ProductDetailSerializer(ProductListSerializer):
description = serializers.CharField()
related_count = serializers.SerializerMethodField()
class Meta(ProductListSerializer.Meta):
fields = ProductListSerializer.Meta.fields + ["description", "related_count"]
def get_related_count(self, obj):
return obj.category.products.filter(is_published=True).exclude(pk=obj.pk).count()
SerializerMethodField 只读,方法名 get_<field_name>;context["request"] 用于拼完整封面 URL(ViewSet 默认已注入)。
13.4 列表/详情不同 Serializer
# catalog/views.py — ch07 已介绍,本章强化字段划分
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.select_related("category").all()
def get_serializer_class(self):
if self.action == "list":
return ProductListSerializer
if self.action == "retrieve":
return ProductDetailSerializer
if self.action in ("create", "update", "partial_update"):
return ProductWriteSerializer
return ProductDetailSerializer
| action | Serializer | 说明 |
|---|---|---|
list | ProductListSerializer | 无长描述,含缩略图 |
retrieve | ProductDetailSerializer | 含 description、关联数 |
create / update | ProductWriteSerializer | 含校验与嵌套写入 |
13.5 ListSerializer 与批量创建
class ProductBulkCreateSerializer(serializers.ListSerializer):
def create(self, validated_data):
objs = [Product(**item) for item in validated_data]
return Product.objects.bulk_create(objs, batch_size=100)