在 PHP 项目里,经常会把前端传来的结构化数据以 JSON 字符串形式存进 MySQL 的 TEXT 或 JSON 字段,读取时再还原成数组或对象。当 JSON 内部还嵌套着另一段 JSON 字符串时,一次简单的 json_decode 往往不能把所有层级都解析开,导致业务层拿到的是字符串而不是可用的数组。

为什么嵌套 JSON 字符串容易解析失败
嵌套 JSON 通常指某一层的字段值本身又是一段 JSON 文本。比如用户资料里有一个字段叫 config,它存的是 {"theme":"dark","prefs":"{"font":12,"notify":true}"}。这里 prefs 在入库前已经被编码成字符串,外层的 config 编码后,prefs 内部的双引号会被转义。如果直接用 json_decode 解外层,prefs 得到的是字符串,不是数组。
另一个常见原因是入库时重复编码。有些开发者在写入前对数组做了一遍 json_encode,框架的 ORM 又自动 encode 了一次,数据库中实际存了两次编码后的文本。读取后只解一次码,内容仍然是字符串,表现为 var_dump 看到的是 string 类型而非 array。这类问题在调试接口时特别隐蔽,因为 json_last_error 可能不会报错,只是类型不对。
基础解析与错误检查
读取数据库字段后,第一步永远是确认数据类型并做错误检查。PHP 的 json_decode 在失败时返回 null,但 null 也可能是合法数据,所以必须用 json_last_error 判断。下面是一段最基础的解析代码:
<?php
$row = $pdo->query("SELECT data FROM logs WHERE id=1")->fetch();
$raw = $row['data']; // 数据库里读出的字符串
$first = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo "第一层解析失败:" . json_last_error_msg();
} else {
var_dump($first);
}
?>
这段代码把字符串转成关联数组,并通过 json_last_error_msg 输出具体错误。如果数据里包含嵌套 JSON 字符串,此时 $first['prefs'] 仍是字符串,需要继续处理。很多初学者忽略这一点,直接把 $first 传给视图层,前端拿到 prefs 后还要再 JSON.parse 一次,增加了耦合。
为了避免重复解析分散在代码各处,建议把嵌套解码逻辑封装成函数。这样在 Service 层统一调用,控制器拿到的一定是完全展开的多维数组,降低出错概率。
递归解析嵌套 JSON 字符串
针对不确定嵌套层数的情况,可以使用递归函数。思路是:对解码后的每个值,如果是字符串且能再次 json_decode 成功,就继续解;如果是数组或对象,就遍历其子元素。下面是一个安全的递归解析实现:
<?php
function deep_json_decode($data, $depth = 0) {
if ($depth > 10) { // 防止恶意深层嵌套导致栈溢出
return $data;
}
if (is_string($data)) {
$tmp = json_decode($data, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($tmp)) {
return deep_json_decode($tmp, $depth + 1);
}
return $data;
}
if (is_array($data)) {
$result = array();
foreach ($data as $key => $value) {
$result[$key] = deep_json_decode($value, $depth + 1);
}
return $result;
}
return $data;
}
$row = $pdo->query("SELECT data FROM logs WHERE id=1")->fetch();
$raw = $row['data'];
$outer = json_decode($raw, true);
$clean = deep_json_decode($outer);
print_r($clean);
?>
上面的 deep_json_decode 先处理外层解码结果,遇到字符串会尝试再解一次,成功且为数组就继续向下。通过设置深度上限,能防御数据库中有人存了几十层嵌套的攻击数据。实际项目中可以把深度上限写成配置项。
要注意的是,并非所有字符串都该解。比如用户昵称里刚好有 {"a":1} 这种内容,误判为 JSON 会带来错误。因此在递归里我们增加了 is_array($tmp) 判断,只有解码后确实是数组才继续,普通文本原样返回。这比单纯用 @json_decode 忽略错误要稳妥得多。
入库前如何避免重复编码
解析麻烦,多半是写入时就埋了坑。如果使用 PDO 且字段是 TEXT,直接 bindValue 字符串即可;若用 Laravel 等框架的 JSON 字段类型,框架会自动 encode,这时就不要手动 json_encode。下面展示一个容易出错的写法与正确写法对比:
<?php
// 错误:手动 encode 后框架又 encode 一次
$userInput = array('name' => 'tom', 'cfg' => json_encode(array('a'=>1)));
// 假设 ORM 会自动把数组转 JSON,这里 cfg 已经是字符串,入库后变转义字符串
// 正确:保持数据为纯数组,让层负责编码
$save = array(
'name' => 'tom',
'cfg' => array('a' => 1) // 内部嵌套数组,不要先 encode
);
// 使用 PDO 时显式编码一次
$jsonStr = json_encode($save);
$stmt = $pdo->prepare("INSERT INTO t(data) VALUES(?)");
$stmt->execute(array($jsonStr));
?>
第一种写法会让 cfg 在数据库里变成带斜杠的字符串,读取后必须解两次。第二种写法结构清晰,读取时只需按前面的递归函数处理真正的嵌套需求,而不是处理人为制造的重复编码。
如果必须存储已编码的字符串(例如第三方回调原文),建议在字段命名上加以区分,比如叫 raw_json,并在解析层明确只解外层,内部留作字符串给专门业务逻辑处理,不要全局递归,避免把签名串、加密串误当作 JSON 解开。
性能与适用场景分析
递归解析在嵌套层次浅、数据量小时开销可以忽略。但当单条记录超过几百 KB,且嵌套遍布各个字段时,每次读取都递归会消耗 CPU。此时可以考虑在写入时就把嵌套展平,或者用 MySQL 5.7 以上的 JSON 类型配合 JSON_EXTRACT 在 SQL 层提取,减少 PHP 端解析压力。
下表列出两种方案的简要对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| PHP 递归解码 | 逻辑灵活,兼容老版本 MySQL | 大数据量时 CPU 占用高 |
| 数据库 JSON 类型+SQL 提取 | 查询快,可建虚拟列索引 | 需 MySQL 5.7+,语法学习成本 |
对于内部管理系统或脚本任务,PHP 端递归足够;对于高并发 API,建议把嵌套结构尽量扁平化,或者改用 JSON 字段配合 SQL 函数。无论哪种方式,读取后做类型断言和错误日志都是必须的,这样线上遇到脏数据能快速定位。
小结与实践建议
处理数据库中嵌套 JSON 字符串的核心是先搞清楚写入形态,再决定解码策略。推荐统一封装 deep_json_decode 并在入口处校验,同时给递归加深度限制。写入时避免手工与框架双重编码,字段含义要清晰。把这几条落到代码规范里,就能彻底告别 json_decode 返回 null 或字符串的诡异问题。
最后提醒,生产环境不要直接用 var_dump 排查,应写监控日志记录 json_last_error_msg 与原始片段。这样当运营反馈某项配置不生效时,开发能从日志里看到是哪一层的引号转义出了错,而不是盲目加 json_decode。
PHPJSON解析nested_json修改时间:2026-08-05 12:33:46