14.优化表单验证失败错误提示信息
现在我们已经做到:
- 用
ModelForm写了表单 - 用
clean_xxx做了字段验证 - 用
fetch提交表单 - 后端能返回 JSON
我可以明确告诉你一句话:
你已经超过 80% 的 Django 初学者了。
现在剩下的,不是“技术难度”,而是体验打磨。
1. 问题出现在哪里?
目前的流程是这样的:
- 用户点击「保存」
- 前端用
fetch提交 - 后端验证失败
- 后端返回 JSON(包含错误信息)
但问题在于:
- 后端返回的是一大坨错误信息
- 前端只知道“失败了”
- 用户不知道哪个字段错了、错在哪里
如下图所示。

本节,我们必须把“开发者能看懂的错误”,翻译成“普通用户能看懂的提示”。
2. 把错误“翻译成人话”
前面章节中,我们已经在students/views.py定义了form_invalid()方法, 它会将错误信息以JSON形式返回到前端。所以,接下来,我们只需要处理前端即可。
做法是三步:
1️⃣ 拿到 errors
上一节我们看到,后端返回的错误信息如下:
{
"status": "error",
"messages": "{\"student_name\": [{\"message\": \"\\u8bf7\\u586b\\u5199\\u6b63\\u786e\\u7684\\u5b66\\u751f\\u59d3\\u540d\", \"code\": \"\"}], \"student_number\": [{\"message\": \"\\u5b66\\u53f7\\u957f\\u5ea6\\u5e94\\u4e3a19\\u4f4d\\u3002\", \"code\": \"\"}], \"birthday\": [{\"message\": \"\\u8f93\\u5165\\u4e00\\u4e2a\\u6709\\u6548\\u7684\\u65e5\\u671f\\u3002\", \"code\": \"invalid\"}], \"contact_number\": [{\"message\": \"\\u8054\\u7cfb\\u7535\\u8bdd\\u5e94\\u4e3a11\\u4f4d\\u3002\", \"code\": \"\"}]}"
}
所以我们需要把messages正确解析出来,代码如下:
// 解析嵌套的 JSON 字符串
const errors = JSON.parse(data.messages);
2️⃣ 拼成一个 HTML 列表
解析出来的messages包含了所有错误信息,我们让每个错误显示一行,所以可以使用HTML中的 <li> 标签。代码如下:
// 构造错误信息的文本
let errorMessage = '';
// 遍历 errors 对象的所有属性(字段名)
for (const field in errors) {
// hasOwnProperty 检查:确保这个属性是对象自身的(不是从原型链继承的)
// 这是 for...in 循环的经典防御性写法(现在不常用,但仍然安全)
if (errors.hasOwnProperty(field)) {
// errors[field] 通常是一个数组,包含该字段的所有错误信息
// 例如:errors.student_name = [{message: "姓名不能为空"}, {message: "姓名格式错误"}]
errors[field].forEach(error => {
// 每次循环都往 errorMessage 里追加一条 <li> 错误提示
errorMessage += `<li style="color:red;text-align:left;margin-left: 100px;">
${error.message}
</li>`;
});
}
}
这段代码是一个典型的错误消息收集与 HTML 拼接片段, 主要目的是把后端返回的字段级错误信息,转换成可直接插入页面的 HTML 列表。
3️⃣ 用 SweetAlert2 一次性展示
Swal.fire({
icon: 'error',
title: '提交失败',
html: errorMessage,
confirmButtonText: '关闭'
});
**注意:**前面我们使用的Swal.fire中的text, 现在
效果你已经在视频里看到了:
- 所有错误一次性列出来
- 清清楚楚
- 用户知道下一步该怎么改

### 4️⃣ 完整代码
student_form.htlm完整代码如下:
# 代码位置:templates/students/student_form.html
`{% load static %}`
<link rel="stylesheet" href="{% static 'css/form.css' %}">
<link rel="stylesheet" href="{% static 'css/sweetalert2.css' %}" >
<div class="container">
{% if student.pk %}
<h2>编辑学生信息</h2>
{% else %}
<h2>添加学生信息</h2>
{% endif %}
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}:</label>
{{ field }}
{% if field.help_text %}
<small class="form-text text-muted"> {{ field.help_text}} </small>
{% endif %}
</div>
`{% endfor %}`
<div class="handleButton">
<button type="submit" id="saveButton">保存</button>
<button type="button" id="cancelButton" onclick="window.parent.Swal.close();">取消</button>
</div>
</form>
</div>
{% if student.pk %}
<script>
var actionUrl = "{% url 'student_update' student.pk %}";
</script>
{% else %}
<script>
var actionUrl = "{% url 'student_create' %}";
</script>
{% endif %}
<script src="{% static 'js/sweetalert2.js' %}" ></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const url = actionUrl;
form.addEventListener('submit', async function (e) {
e.preventDefault(); // 阻止默认提交
const formData = new FormData(form);
try {
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': formData.get('csrfmiddlewaretoken'),
},
credentials: 'same-origin' // 重要:携带 cookie
});
const data = await response.json();
if (data.status === 'success') {
Swal.fire({
icon: 'success',
title: data.message || '提交成功',
text: '学生信息已成功保存',
timer: 2000,
showConfirmButton: false
});
} else {
// 解析嵌套的 JSON 字符串
const errors = JSON.parse(data.messages);
// 构造错误信息的文本
let errorMessage = '';
for (const field in errors) {
if (errors.hasOwnProperty(field)) {
errors[field].forEach(error => {
errorMessage += `<li style="color:red;text-align:left;margin-left: 100px;">
${error.message}
</li>`;
});
}
};
Swal.fire({
icon: 'error',
title: '提交失败',
html: errorMessage,
confirmButtonText: '关闭'
});
}
console.log('服务器返回:', data);
} catch (error) {
console.error('请求出错:', error);
Swal.fire({
icon: 'error',
title: '网络错误',
text: '请检查网络连接或稍后重试'
});
}
});
});
</script>
3.为什么我强烈推荐这样做?
说句实话,这是我踩过无数坑以后才总结出来的经验。
如果你只做“单字段提示”:
- 用户要改一次、提交一次
- 特别容易烦
- 真实项目中非常容易被投诉
如果你一次性展示全部错误:
-
用户效率高
-
页面专业感直接上一个台阶
到这里,我们完整走通了一条“专业级表单路径”
我们回顾一下你现在已经掌握了什么:
- ✅ ModelForm 表单验证
- ✅ clean_xxx 精细校验
- ✅ fetch 异步提交
- ✅ form_invalid 接住错误
- ✅ SweetAlert2 友好提示
这一套流程,就是很多真实后台系统的标准做法。
下一步
别急着往下看课程,我建议你现在立刻做三件事:
- 故意填错 2~3 个字段
- 看 SweetAlert 弹窗里的错误是否清晰
- 自己改一条错误提示文案,让它更“像人说的话”
你会明显感觉到:
你写的已经不是“教学 Demo”,而是“真实系统”。
下一节,我们会继续做一件非常关键的事情:
👉 将验证通过的学生信息保存到数据库。
我们下节见 👋
【大熊课堂精品课程】
Python零基础入门动画课: https://www.bilibili.com/cheese/play/ss7988
Django+Vue:全栈开发: https://www.bilibili.com/cheese/play/ss8134
PyQT6开发桌面软件: https://www.bilibili.com/cheese/play/ss12314
Python办公自动化: https://www.bilibili.com/cheese/play/ss14990
Cursor AI编程+MCP:零基础实战项目课: https://www.bilibili.com/cheese/play/ss105194189
Pandas数据分析实战: https://www.bilibili.com/cheese/play/ss734522035
AI大模型+Python小白应用实战: https://www.bilibili.com/cheese/play/ss3844