杂记 › 技术笔记 › 接口 JSON 贴成 PHP 数组,哪些值会悄悄变形

接口 JSON 贴成 PHP 数组,哪些值会悄悄变形

上周造对账 fixture,我把订单接口的返回贴进转换器转成 PHP 数组。三天后才暴露:日志里 user_id 是 9007199254740993,种子里是 9007199254740992;金额 9223372036854775807 抄出来成了 9223372036854776000。不报错也不告警,值自己变了。

我扒了模板实现、跑完 88 组用例,结论是:绝大多数变形发生在第一步的 JSON.parse,不在拼代码那一步。

变形点:值先落进 JS 的 number

链路很短:读字符串、JSON.parse、按类型拼成 '键' => 值,数字那条分支(模板 L107)就是把 parse 完的 number 直接字符串化。值一旦落进 double,缩进再漂亮也不是原值。2^53=9007199254740992 是 JS 整数的精确上限,边界上原样,边界之上没有保证。控件只有输出格式、缩进和是否加 <?php 标签,没有大整数保真开关。

JSON 里的值转出的字面量PHP 侧 json_decode
90071992547409939007199254740992integer
1234567890123456789012345678901234567000double 1.2345678901234567E+19
92233720368547758079223372036854776000integer,差 193

最后一行最难受:PHP 自己接是原值,只有先过 JS 才塌。订单号、雪花 ID、微秒时间戳基本都是裸数字。

浮点没有统一规律,无穷是算出来的

  • 21 位的 3.14159265358979323846 只剩 16 位的 3.141592653589793。
  • 17 位有效数字内的 0.30000000000000004 和 1.2345678901234567 一字不改。
  • 尾随零被吃掉:1.500 成 1.5,-0 成 0,1.5e3 成 1500。
  • 写法会换:1e20 出 100000000000000000000,1e21 出 1e+21,1e-324 下溢成 0。

超大指数会算出无穷,1e999 出 Infinity、-1e999 出 -Infinity。PHP 里没有 Infinity 字面量、对应常量是 INF,这轮没跑 php -l,我不断言那行能不能编译。

null、空容器、重复键、键序

{} 与 [] 转出来都是 [];切到 PHP 对象那档,空对象连 (object) 前缀都没有,{"a":[{},[]]} 出 'a' => [ [], [], ]。重复键静默合并,{"a":1,"a":2,"a":3} 只剩 'a' => 3,19 字节塌成 14。数字样的键名会重排,{"01":"a","10":"b"} 里 '10' 排在 '01' 前。{"z":null} 出 'z' => null 不塌;{"s":"007"} 也不转成数字、不 trim。

中文键、\u 转义与引号

中文没有障碍:{"用户名称":"张三"} 出 '用户名称' => '张三';保留字、带空格和中划线的键照常输出。\u 在 parse 阶段就被解掉,{"s":"\u4e2d\u6587"} 出来是 2 个汉字。转义只管反斜杠和单引号,{"a'b":"x"} 变 'a\'b',双引号原样落进单引号串。

复制回来的不等于生成的

输出区用 innerHTML 渲染高亮,不转义尖括号和 &,复制按钮拿的却是 textContent。{"s":"a<b>c</b>"} 复制回来是 's' => 'ac',这条未经 DOM 时长度是 24;{"<?php":"x"} 的键名整个消失,只剩 ' => 'x'。所以 diff 要 diff 原文和最终数组。

规模与深度:从慢到崩

条形=耗时(毫秒),文字=输入/输出字节 1000 项 8 毫秒 · 24790 → 54811 10000 项 67 毫秒 · 267790 → 567811 100000 项 742 毫秒 · 2877790 → 5877811 输出/输入倍数 2.21、2.12、2.04

图 1:三档同一模板连跑,err 全是空串,没有静默失败也没有截断告警。

层数带来的不是慢而是崩:depth=100 输出 20401 字符,depth=1000 出 2004001,depth=2000 出 8008001,depth=5000 起就是 Maximum call stack size exceeded。缩进每层多 2×depth 个空格,总长按平方顶上去,可用栈深落在 1000 与 5000 之间;对象嵌套阈值给不出,那几条用例本身非法。

嵌套深度与输出字符数 depth=100 20401 depth=1000 2004001 depth=2000 8008001 depth=5000 → Maximum call stack size exceeded

图 2:深度每上一档,输出字符按平方涨。

报错只有一行英文

红框标题固定是 JSON 解析错误,正文就是 e.message,输出区与统计一起清空。{"n":007} 报 Unexpected number...position 6,{"a":1,} 报 Expected double-quoted property name...position 7;{"a":1}{"b":2} 报 Unexpected non-whitespace character...position 7,一次只认一个文档。空白输入只显示空态,带 BOM 的照常出;报错是否本地化我没有样本。一贴就报 position 1 的,我先怀疑原文,拿 JSON 格式化 试一遍确认它本身合不合法。

我现在的核对姿势

整段接口返回落成 PHP 数组我用的是 JSON 转 PHP 数组,上面的数字都出自它同一份模板。之后固定三个动作:

  1. 2^53 以上的字段别过 JS,在 PHP 里读原文 json_decode 再自己导出。
  2. 转完比长度不比样子,莫名变短的八成被显示层吃掉了。
  3. 键序和空容器单独验,光看代码看不出来;核原文层级我按 树形视图折叠定位那篇 的做法。

PHP 对象那档只给走对象分支的层加 (object) 前缀,顶层数组和标量都不加,输出里没有 stdClass 字样。

Q:为什么 20 位整数一定会变,0.30000000000000004 反而一字不改?

A:取决于能不能被 double 精确表示,不取决于长得怪不怪。超出 2^53 的整数只能落到最近的可表示值上;而 0.30000000000000004 是 double 打印出来的样子。

Q:{} 和 [] 转出来一样,怎么知道原文是哪个?

A:转出的代码里问不出来:两条分支连字节数都相同,对象那档也救不了空对象,只能回原文核。

Q:只转出一半,是被截断了吗?

A:先分清少的是尾巴还是中间,带尖括号少中间那种跟体积无关。最大样本 100000 项、输出 5877811 字节,这一档没截断也没告警,更大的没实测。

📎 相关文章

PNG 压了三遍只减 426 字节:无损压缩的真实边界 2026-10-22 WebP 转 PNG 后体积涨了几倍:什么时候必须转,怎么把涨幅压住 2026-10-22 截图缩到四分之一,小字糊成一团的三种原因 2026-10-22 同样裁 300×300,头像为什么总偏右下角半格 2026-10-22 按每月 1.7 倍预估 18 个月,脚本和表格为什么给出两个数 2026-10-22