PAYATHON 2026

PHP 与 Javascript API 交互时,何时需要对 JSON 编码或解码?

排障赵师傅

结论

在当前调用配置下,移除 JSON.stringify() 后就能正常得到数组,因此应直接返回 JavaScript 对象,让 RESTlet 的响应处理层完成序列化。PHP 收到响应后,调用一次 json_decode($response, true) 即可。

题述现象通常是响应被重复 JSON 编码造成的:JSON.stringify() 先把 JavaScript 对象转成字符串,RESTlet 的响应处理层又把这个字符串编码成 JSON。这样,PHP 调用一次 json_decode() 后,得到的仍是字符串。

是否需要编解码,取决于当前数据是对象还是 JSON 文本,以及框架是否已经完成了转换。

为什么解码后仍然是字符串?

下面两段都是合法 JSON,但顶层类型不同。

第一段表示对象:

{"result":"success","data":{"test":"test"}}

第二段表示字符串,字符串的内容又是一段 JSON:

"{\"result\":\"success\",\"data\":{\"test\":\"test\"}}"

PHP 对它们解码后,得到的类型也不同:

$objectJson = '{"result":"success","data":{"test":"test"}}';
$stringJson = '"{\"result\":\"success\",\"data\":{\"test\":\"test\"}}"';

var_dump(json_decode($objectJson, true)); // array
var_dump(json_decode($stringJson, true)); // string

json_decode() 只解析一层 JSON,不会继续解析结果字符串中的 JSON 文本。第二个参数 true 表示将 JSON 对象转换为关联数组,不会把所有类型的结果都强制转换为数组。

在 JavaScript 中,连续编码两次的结果如下:

var value = {
    result: 'success',
    data: { test: 'test' }
};

var once = JSON.stringify(value);
// {"result":"success","data":{"test":"test"}}

var twice = JSON.stringify(once);
// "{\"result\":\"success\",\"data\":{\"test\":\"test\"}}"

如果 RESTlet 又对返回的字符串做了一次序列化,就会出现题述结果。要确认具体是哪一层进行了重复编码,需要检查原始 HTTP 响应正文,仅看 print_r() 不够直观。

先检查实际传输的内容

排查时,先查看 PHP 从 cURL 获取、尚未解码的响应正文,同时检查 HTTP 状态码和响应的 Content-Type。

设置 CURLOPT_RETURNTRANSFER => true 后,可以这样检查:

$response = curl_exec($ch);

if ($response === false) {
    throw new RuntimeException(curl_error($ch));
}

// 仅用于调试;生产环境应避免记录敏感响应。
var_dump($response);

$result = json_decode($response, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException(json_last_error_msg());
}

var_dump($result);

如果原始正文带有前面示例中的外层双引号和转义符,就能确认传输的是 JSON 字符串值。

检查时要区分正文中实际存在的转义符,以及日志工具为了显示字符串而添加的转义符。必要时查看原始响应内容。

PHP 请求和 JavaScript 响应应该怎么写?

PHP:将数组编码一次,再作为请求正文发送

$payload = [
    'result' => 'success',
    'data' => ['test' => 'test'],
];

$body = json_encode($payload);

if ($body === false) {
    throw new RuntimeException(json_last_error_msg());
}

// 假定 $ch 已配置目标地址、鉴权及请求头。
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_RETURNTRANSFER => true,
]);

请求头应包含 Content-Type: application/json,并保留接口要求的鉴权头。也可以通过 Accept: application/json 表明希望接收 JSON 响应。

传给 CURLOPT_POSTFIELDS 的应是编码后的 $body 字符串。直接传入 PHP 数组会触发表单提交处理,不能替代 JSON 请求正文。

JavaScript:根据入口参数类型决定是否解析

RESTlet 的入口参数可能已经由平台解析,应根据接口约定和参数的实际类型处理:

function post(requestBody) {
    var payload;

    if (typeof requestBody === 'string') {
        payload = JSON.parse(requestBody);
    } else {
        payload = requestBody;
    }

    // 适用于题述中直接返回对象即可得到正确响应的配置。
    return payload;
}

将这段入口处理逻辑放入现有的 RESTlet 脚本中。

参数已经是对象时,直接使用即可。JSON.parse() 用来解析 JSON 文本,无需对对象再次调用。请求格式有误时,还应按接口约定处理解析异常,并校验字段。

响应的处理方式也取决于平台:题述配置下直接返回对象即可;如果接口要求开发者自行写入原始响应正文,则需要调用 JSON.stringify(),再将生成的文本写入正文。

PHP:解码一次,并检查返回结构

以下代码适用于 PHP 7.2,假定 $response 已通过 cURL 错误检查,HTTP 状态也符合接口预期:

$result = json_decode($response, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException(
        '响应不是有效 JSON:' . json_last_error_msg()
    );
}

if (!is_array($result)) {
    throw new UnexpectedValueException('响应顶层类型不符合约定');
}

if (
    !isset($result['result'], $result['data']) ||
    !is_array($result['data'])
) {
    throw new UnexpectedValueException('响应字段不符合约定');
}

PHP 7.2 可以用 json_last_error() 和 json_last_error_msg() 检查错误。JSON_THROW_ON_ERROR 从 PHP 7.3 才开始提供,不能直接用于题述版本。

什么时候需要编码或解码?

当前情况应采取的操作
PHP 数组需要成为 JSON 请求正文调用一次 json_encode()
JavaScript 收到原始 JSON 请求文本调用一次 JSON.parse()
JavaScript 收到平台已解析的对象直接使用
框架负责把返回对象序列化为 JSON直接返回对象
需要自行生成原始 JSON 响应正文调用一次 JSON.stringify()
PHP cURL 收到原始 JSON 响应正文调用一次 json_decode()
客户端库已经把响应解析为对象或数组直接使用

同一次请求中,请求正文和响应正文是两份独立的数据,需要分别完成序列化与解析。业务代码是否需要显式调用这些函数,要看框架已经完成了哪些步骤。

文档示例、Python 和签名需要注意什么?

文档中出现 JSON.stringify() 时,要结合完整示例判断返回值要求的是字符串还是对象,并检查示例是否直接写入响应正文、是否有额外的响应封装。题目没有给出具体文档页面和配置,因此无法判断该示例是否适用于当前端点。

Python 的 JSON 编解码遵循同样的类型规则。如果同类代码的表现不同,应比较实际发送和接收的正文,并检查 HTTP 客户端或服务端框架是否自动处理了 JSON。

HMAC-SHA256 不会直接导致解码结果变成字符串。如果签名协议包含请求正文,应按协议用最终发送的正文计算签名,避免在签名后重新编码。空格、转义方式或字段顺序发生变化,都可能改变参与签名的字节。

题目中的写法:

{'result': 'success', 'data': {'test': 'test'}}

是 JavaScript 对象字面量,但不是合法的 JSON 文本。JSON 的属性名和字符串值必须使用双引号。

只有接口明确约定返回“包含 JSON 文本的字符串”时,才应进行第二次解码,不要将连续解码两次作为默认修复方式。如果接口两端都由自己维护,应约定响应顶层为对象,并修正多余的序列化操作。

备注:内容仅供参考。