/ 👨‍💻 代码敲不完 / 34浏览

如何验证一个 JSON 是否合法

1. 前言

在日常开发过程中,JSON 已经是一个使用场景非常广泛的数据格式。我们有很多好用的 JSON 解析库,Jackson、Gson、fastjson 等等,在以上提到的库中,已经具备了非常完备的校验方案,作为一个有追求的程序员,本文会从学习的角度出发,学习如何通过栈结构校验一个 JSON 是否合法。

我们的核心需求:不依赖重量级 JSON 库,实现一个轻量级 JSON 格式校验工具。

2. JSON 的语法与常见错误陷阱

语言规则要点

  • 核心结构:根元素只能是对象 {}数组 []

  • 键名必须双引号包裹:如 "name": "John",而 name: "John" 是非法的

  • 嵌套与分隔符[ [1,2], {"k":"v"} ] 合法,但 {123}(键为数字)、[ "a", ](尾逗号)是非法的

  • 转义字符:双引号在字符串内需写成 \\"

我们遇到的真实错误案例

  1. 测试工程师写测试用例时忘记闭合对象
    "{ 'key1': 'v1', 'key2': [1,2"} → 直接导致 REST 接口日志雪崩

  2. 前端传参数时数组与对象混淆
    {"options": {0: "a", 1: "b"}} → 键名用数字未加引号

  3. 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. 现实场景中的错误校验能力

在项目中,栈的这一特性直接解决了三个高频问题:

问题 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 约束),还需在此基础上继续完善。

在数据采集中使用对象池的实践
在数据采集中使用对象池的实践
在业务中使用 Kafka 到底能不能保证消息的有序性
数据处理中的责任链模式
数据处理中的责任链模式
探索 Kafka 消息丢失的问题和解决方案
SpringBoot 中实现订单过期自动取消
SpringBoot 中实现订单过期自动取消
Java 程序优化之-如何更好的利用CPU