Skip to content

原生 JSON API 与长期契约 ​

本站纵览 | 练习答案

python
# tickets/api.py
import json
from json import JSONDecodeError

from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods

from .models import Ticket


def serialize_ticket(ticket):
    return {
        "id": ticket.pk,
        "title": ticket.title,
        "status": ticket.status,
        "priority": ticket.priority,
        "updated_at": ticket.updated_at.isoformat(),
    }


@login_required
@require_http_methods(["GET", "POST"])
def ticket_collection(request):
    if request.method == "GET":
        rows = Ticket.objects.filter(assignee=request.user).order_by("-id")[:50]
        return JsonResponse({"version": 1, "results": [serialize_ticket(row) for row in rows]})

    if request.content_type != "application/json":
        return JsonResponse({"error": {"code": "unsupported_media_type"}}, status=415)
    try:
        payload = json.loads(request.body)
    except (JSONDecodeError, UnicodeDecodeError):
        return JsonResponse({"error": {"code": "invalid_json"}}, status=400)
    allowed = {"title", "priority"}
    unknown = sorted(set(payload) - allowed)
    if unknown:
        return JsonResponse(
            {"error": {"code": "unknown_fields", "fields": unknown}}, status=400
        )
    ticket = Ticket(
        title=payload.get("title", ""),
        priority=payload.get("priority", 3),
        assignee=request.user,
    )
    ticket.full_clean()
    ticket.save()
    return JsonResponse(serialize_ticket(ticket), status=201)

深水边界 ​

  • 示例需在统一异常层把 ValidationError 转成稳定 400;不要让堆栈成为协议。
  • 列表上限不是完整分页。生产接口需要稳定排序、游标/页码合同与最大页大小。
  • API 版本优先通过兼容新增演进;删除/改义要有弃用窗口和调用方证据。
  • request.content_type、字符集、请求体上限和代理配置共同构成输入边界。
  • DRF 可提供 serializer、permission、pagination 和 schema;引入前检查当前 DRF 对 Django 6.0 的正式支持。

验收 ​

内容类型、JSON 语法、字段校验与对象权限各自返回稳定状态和错误码;响应字段集合由契约测试锁定。

练习(先独立完成) ​

  1. 捕获 ValidationError,把字段错误转换成固定 JSON 结构。
  2. 增加 next_after 游标并处理非法游标。
  3. 为输出字段写精确集合断言,而不是只断言包含 title。

完成后核对答案。

深度加餐:API 响应是给未来客户端看的合同 ​

字段名、状态码、错误形状和幂等语义一旦发布,就会被别人当成地基。

先做一个可观察实验 ​

python
from django.http import JsonResponse
from django.views.decorators.http import require_GET

@require_GET
def ticket_summary(request):
    rows = list(
        Ticket.objects.filter(owner=request.user)
        .order_by('-created_at', '-pk')
        .values('id', 'title', 'status')[:20]
    )
    return JsonResponse({
        'data': rows,
        'meta': {'count': len(rows), 'version': 1},
    })

预期:JSON 结构稳定,查询只取 API 需要的字段;状态码与 data/meta 分工清楚。

原理下潜与工程边界 ​

API 层负责认证、输入解析、领域调用和错误映射;不要把 ORM Model 直接当公开序列化格式。时间、金额、枚举和 null 都要定义可机器解析的约定。

JsonResponse 默认要求字典;用户输入不能直接拼接 JSON;PATCH 的部分更新与 PUT 的整体替换语义不同;缓存时要考虑 Vary、ETag 和权限隔离。

反例排错路线 ​

用 Client 检查 Content-Type、状态码、字段类型和错误响应;契约测试应固定必需字段,不要把整个 JSON 文本 snapshot 成一块。

练习加码(仍不提供答案) ​

加码:为创建工单 API 设计幂等键、422 校验错误和 409 冲突响应,并写出一组契约测试。

验收时请补充:正常路径、权限或失败路径、以及一条能证明事务/资源/配置边界没有被绕过的测试。

继续查阅:Django 官方文档(外部资料已收录于证据账本) · Django 安全清单(外部资料已收录于证据账本)

课程合同:原生 JSON 与 DRF 边界 ​

学习目标 ​

学完本篇,读者应能把「JsonResponse 只输出公开字段,错误和认证状态码稳定」拆成输入、可信边界、Django 机制和可复查证据;同时能说明 Django 6.0 与 5.2 LTS 的差异不会改变业务不变量。

心智模型 ​

请求/数据输入 → 边界检查 → Django 核心机制 → 可观察输出 → 恢复动作。配置、数据库提交和外部副作用分别拥有自己的生命周期;没有证据的“应该可以”不算完成。

本地机制图 ​

原生 JSON 与 DRF 边界机制图

图中的“输入”是不可信的 HTTP、表单、任务或配置;“边界”负责身份、格式、权限、大小、超时与版本;“机制”才是 URL、View、ORM、事务、缓存、Tasks 或 ASGI;“证据”是测试、响应、日志、查询计划和发布记录。

最小实验 ​

在全新虚拟环境中运行:

bash
python -m django --version
python manage.py check
python manage.py test

实验只验证一个机制:输入一条有效值,再输入一条空值、重复值或无权限值。每次实验都记录 Django 版本、数据库、命令和退出码。

完整场景 ​

把实验接到 tickets 工单流:请求携带用户身份和工单 ID,经过当前专题负责的边界,完成一次读或写,并留下响应状态、数据库变化、日志字段和清理动作。完整场景必须能第二次执行,或明确说明幂等键、事务回滚、缓存失效和任务重试策略。

运行输出 ​

成功路径应出现:

text
Django 6.0.x
System check identified no issues (0 silenced).
OK

失败路径要保留真实信号,例如 400(输入无效)、403(权限/CSRF)、404(资源不存在)、405(方法不允许)或数据库约束异常;不要把失败改写成含糊的 200。

失败边界 ​

至少验证:空输入、重复提交、匿名用户、越权对象、并发写入、下游不可用、超时和版本不兼容。记录“谁拒绝、何时拒绝、状态是否回滚、重试是否安全”。开发模式的 traceback 不得进入生产响应,缓存不得跨用户复用敏感结果。

原理与排错 ​

先确认版本和 settings,再用 manage.py check 定位启动问题;接着确认 URL/任务入口、输入解析和权限;最后检查 SQL、事务提交、响应和副作用。若结果异常,用最小复现打开 SQL/请求 ID/任务 ID 观测,沿「复现 → 缩小 → 观测 → 修复 → 回归」闭环,不靠重装依赖猜测。

练习(正文不泄答案) ​

  1. 为上述完整场景补一条正常路径和两条失败路径测试,并记录完整输出。
  2. 画出本篇机制图,标出身份 owner、数据提交点、缓存/任务副作用和恢复动作。
  3. 故意制造一次重复、越权或超时,说明应返回的状态、日志字段和是否可重试。
  4. 写一条 Django 5.2 LTS 兼容验收标准,再说明 Django 6.0 的新能力是否需要条件分支。

验收标准:命令可重复、输出可复查、正文没有练习答案;完成后再打开本阶段答案册。

答案册 ​

本阶段答案册提供参考实现、关键推理、失败边界和替代方案;先独立完成练习再对照。

下一篇 ​

下一篇是06-apis-and-contracts/02-pagination-errors-versioning,继续沿同一条输入—边界—机制—证据链前进。

用心记录每一步成长