1. 前言
在日常开发过程中,JSON 已经是一个使用场景非常广泛的数据格式。我们有很多好用的 JSON 解析库,Jackson、Gson、fastjson 等等,在以上提到的库中,已经具备了非常完备的校验方案,作为一个有追求的程序员,本文会从学习的角度出发,学习如何通过栈结构校验一个 JSON 是否合法。
我们的核心需求:不依赖重量级 JSON 库,实现一个轻量级 JSON 格式校验工具。
2. JSON 的语法与常见错误陷阱
语言规则要点
-
核心结构:根元素只能是对象 {} 或数组 []
-
键名必须双引号包裹:如
"name": "John"
,而name: "John"
是非法的 -
嵌套与分隔符:
[ [1,2], {"k":"v"} ]
合法,但{123}
(键为数字)、[ "a", ]
(尾逗号)是非法的 -
转义字符:双引号在字符串内需写成
\\"
我们遇到的真实错误案例
-
测试工程师写测试用例时忘记闭合对象:
"{ 'key1': 'v1', 'key2': [1,2"}
→ 直接导致 REST 接口日志雪崩 -
前端传参数时数组与对象混淆:
{"options": {0: "a", 1: "b"}}
→ 键名用数字未加引号 -
CSV 导入 JSON 时出现特殊符号:
"note": "异常字符:\\r\\n"
→ 转义不正确导致键值解析失败
栈结构的巧妙使用:为何要选择栈作为 JSON 校验的核心工具?
这是一个关于「为什么选择栈,而非其他数据结构」的设计思考,也是我从无数调试崩溃中悟出的重要经验。让我们从一个实际案例切入:
关键问题:JSON 中的嵌套闭合困境
{
"order": {
"items": [
{"name": "iPhone", "price": 999},
{"name": "AirPods"...
]
}
以上 JSON 中,因为最终的}]}
闭合顺序出错了,会导致 JSON 解析报错,这种嵌套闭合问题在 JSON 中极其常见
-
对象
{
必须与}
配对 -
数组
[
必须与]
配对 -
且闭合顺序必须严格遵循「后开先关」规则(Last-In-First-Out)。
此时,一个能精准追踪层级关系的数据结构就至关重要。
为何栈(Stack)是不二之选?
1. 括号匹配的天然适配
栈的「先进后出」特性,完美贴合 JSON 嵌套的本质:
-
遇到左括号
(
「{」「[」
→ 入栈保存层级信息 -
遇到右括号
)「}」「]」
→ 弹出栈顶元素并校验类型匹配
如此循环,栈始终记录了当前需要闭合的「最近未闭合的符号」。只要栈最终为空(且未在字符串中),则说明嵌套完全闭合。
2. 摒弃复杂算法的简单之道
试想过用「双指针」或「计数器」追踪括号吗?
-
计数器的局限性:无法处理交叉嵌套类型(如
{ [...{...}] }
)——单纯统计{
和}
的数量无法检测到}
是否提前闭合了错误符号。 -
栈的优雅之处:无需额外逻辑,仅通过入栈、出栈动作自然实现匹配。例如下面的代码片段:
Stack<Character> stack = new Stack<>();
stack.push('{'); // 压入对象开始的符号
stack.push('['); // 嵌套进数组符号
char closeToken = ']'; // 现在遇到了 ]
if (stack.pop() != '[') { // 弹出并判断是否匹配
return false; // 不匹配直接失败
}
-
现实场景中的错误校验能力
在项目中,栈的这一特性直接解决了三个高频问题:
问题 1:括号类型错配
{
"k": [1,2} // 数组 `[` 用 `}` 结尾!
-
栈存储了
[
,弹出时发现}
与[
不同 → 立即报错。
问题 2:多层嵌套未闭合
{ "items": [{ "id": "A" }, // 少一个 `]` 和 `}`
-
栈保留了
{
和[
,最终栈不为空 → 判定结构非法。
问题 3:乱序闭合
{ [..."]]} // ] 闭合对象,} 闭合数组 → 栈弹出顺序混乱
-
弹出栈顶符号
{
与]
不匹配 → 马上终止校验。
3. 算法实现
核心代码与逻辑拆分
public static boolean validate(String jsonStr) {
if (jsonStr == null || jsonStr.isEmpty()) return false;
if (!isValidStartChar(jsonStr.charAt(0))) return false; // 第1道防线
Stack<Character> stack = new Stack<>();
boolean inString = false, escape = false;
for (int i = 0; i < jsonStr.length(); i++) {
char c = jsonStr.charAt(i);
// 状态转移:字符串内部逻辑
if (c == '"' && !escape) {
inString = !inString; // 切换字符串状态
escape = false; continue;
}
if (c == '\\' && inString) {
escape = !escape; // 转义符开关
continue;
}
// 非字符串内外层处理:括号匹配
if (!inString) {
if (isOpenBracket(c)) stack.push(c);
else if (isCloseBracket(c)) {
if (stack.isEmpty() || !ismatch(stack.peek(), c))
return false; // 括号类型或缺少左括号
stack.pop();
}
}
}
// 最终检验:栈空+字符串已闭合
return stack.isEmpty() && !inString;
}
4. 总结
JSON 格式校验就像 JSON 的 体检第一关,拦截的虽是表面问题,但能避免大量崩溃案例。当然,若需深度校验(如值类型、Schema 约束),还需在此基础上继续完善。