Django 返回 JSON 的三种方式与 4 个真实坑:中文被转义、safe=False、日期不可序列化、接口返回了 HTML
Django 返回 JSON:三种写法,四个坑
本站的 /api/v1/posts/ 是自己写的 REST 接口(Django 4.2 + DRF),
发布、上传、取文章都走它。写的过程中把"返回 JSON"这件小事的坑基本踩了一遍——
它们都不是什么高深问题,但每一个都会让前端 JSON.parse 直接崩。
这篇不讲大而全,只讲:三种写法分别在什么场景下用,以及四个我真实遇到过的坑。
一、结论先行:怎么选
| 你的场景 | 用什么 | 关键点 |
|---|---|---|
| 纯 Django 函数视图,返回简单 dict | JsonResponse |
返回 list 要 safe=False;中文要关 ensure_ascii |
| DRF 类视图 | APIView + Response |
自动序列化,支持内容协商 |
| DRF 函数视图 | @api_view + Response |
同上,写起来更短 |
~~HttpResponse(json.dumps(...))~~ |
❌ 别用 | Content-Type 是 text/html,编码与类型全要自己管 |
二、三种写法
1. 函数视图:JsonResponse
from django.http import JsonResponse
def ping(request):
return JsonResponse({"status": "ok", "code": 0})
它做对了两件事:Content-Type 自动设为 application/json,并且帮你调 json.dumps。
2. 类视图:DRF 的 APIView
from rest_framework.views import APIView
from rest_framework.response import Response
class PingView(APIView):
def get(self, request):
return Response({"status": "ok"})
3. 函数式 DRF:@api_view
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["GET"])
def ping(request):
return Response({"status": "ok"})
三、四个真实坑
坑 1:中文全变成了 \uXXXX
同样是 JsonResponse({"msg": "发布成功"}),前端拿到的是:
{"msg": "\u53d1\u5e03\u6210\u529f"}
根因:json.dumps 默认 ensure_ascii=True,会把非 ASCII 字符转义成 Unicode 码点。
功能上没错(JSON.parse 能还原),但抓包调试时看不懂,日志里也全是转义串。
解法:
return JsonResponse(
{"msg": "发布成功"},
json_dumps_params={"ensure_ascii": False},
)
DRF 侧则在 settings.py 里关掉:
REST_FRAMEWORK = {
"UNICODE_JSON": False, # 默认 True,会转义中文
}
坑 2:返回列表时报错 safe
return JsonResponse([1, 2, 3])
TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False
根因:JsonResponse 出于安全考虑,默认只接受 dict(历史上 list 响应曾被 JSON hijacking 利用)。
解法:确实要返回数组时显式声明:
return JsonResponse([1, 2, 3], safe=False)
坑 3:datetime / Decimal 不可序列化
接口要返回文章创建时间,直接塞 datetime 进去:
return JsonResponse({"created_at": post.created_at})
TypeError: Object of type datetime is not JSON serializable
json 模块只认原生类型(dict/list/str/int/float/bool/None),
datetime、Decimal、Django 的 QuerySet 都不在名单里。
三种解法,按场景选:
# A. 自己转成字符串(最简单,适合字段少)
{"created_at": post.created_at.strftime("%Y-%m-%d %H:%M:%S")}
# B. 用 DRF 的 serializer(字段多、要校验时用这个)
# serializer 会自动把 datetime 格式化、Decimal 转字符串
# C. 自定义 encoder(要全局生效时)
class MyEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
return super().default(o)
坑 4:浏览器里打开接口,返回的是 HTML
这个最容易被误判成"接口坏了"。
同一个 DRF 接口,浏览器直接访问看到的是带样式的调试页面,
而前端 fetch 拿到的却是正常 JSON。
根因:DRF 的 Response 会做内容协商——根据请求头 Accept 决定渲染格式。
浏览器发的是 Accept: text/html,于是 DRF 渲染成可浏览的 HTML 页面;
fetch / curl 默认不带这个头(或带 application/json),于是拿到 JSON。
这不是 bug,但排查时极易混淆。 验证时别用浏览器,用 curl 看原始响应:
curl -s -i https://example.com/api/v1/ping/ | head -8
重点看这两行:
HTTP/1.1 200 OK
Content-Type: application/json
只要 Content-Type 是 application/json,接口就是对的。
四、一条验证命令
写完接口别急着联调,先用 curl 确认三件事:状态码、Content-Type、响应体。
curl -s -i -H "Accept: application/json" https://example.com/api/v1/ping/
| 检查项 | 期望 |
|---|---|
| 状态码 | 成功 200;参数错 400;未授权 401(不要一律 200) |
Content-Type |
application/json(看到 text/html 说明走错了分支) |
| 响应体 | 能被 jq 或 python -m json.tool 解析 |
状态码语义是最容易偷懒的地方:出错也返回 200、把错误码塞在 body 里,
前端就得靠字符串判断成败。该 4xx 就 4xx,这是 HTTP 的语义,别浪费它。
小结
| 坑 | 典型报错 / 现象 | 解法 |
|---|---|---|
| 中文被转义 | \u53d1\u5e03... |
json_dumps_params={"ensure_ascii": False} / UNICODE_JSON=False |
| 返回列表报错 | ...set the safe parameter to False |
JsonResponse(data, safe=False) |
| 日期不可序列化 | Object of type datetime is not JSON serializable |
转字符串 / DRF serializer / 自定义 encoder |
| 浏览器看到 HTML | DRF 内容协商(Accept 头) | 用 curl 验证,看 Content-Type |
三种写法本身都不难,难的是知道每个坑长什么样。
写完接口花 10 秒跑一次 curl -i,能省掉后面半小时的联调扯皮。
本文环境:Django 4.2 / DRF 3.15 / Python 3.11
报错原文与行为均为 Django / DRF 官方实现,具体参数以官方文档为准。
系列阅读
- 新站百度「普通收录」主动推送实战——本站自研 REST API 的另一半:发布与推送链路。