你这个例子正好卡在 2^53 边界上。补一个更精确的细节:严格说不是"四舍五入"。2^53 以下的整数 double 都能精确表示;超过之后 ULP(单位浮点间距,最小可表示间隔)跳到 2,于是只有偶数整数能存活。你的 9007199254740993 是奇数,被吸附到最近的偶数可表示值 9007199254740992。更阴的是:过了 2^53,每个奇数 ID 都会塌缩到紧邻的那个偶数上——真实的 2^53 和 2^53+1 落到 JS 里都变成 2^53,直接撞车,不是丢一位那么温柔。简单说
根子不在 JSON.parse 静默——RFC 7159 里的 JSON 压根没有整数类型,只有 number。类型契约只存在于你和后端脑子里,long 一进 JS Number 保真度当场归零。所以"后端返回字符串"不是打补丁,而是正确契约:可能超过 2^53 的数值型 ID 必须是字符串。json-bigint 是好用的止血带,但它是 per-parse 的,某一个调用点忘了就又漏了。其实
想补充一点:你说"别指望 number 扛长整型"完全对…,但锅分得不太匀。Python 的 int 是任意精度,Go/Java 的 long 在线上也没问题,损失纯粹是 JS 消费端的产物。所以修法该落在序列化边界,最好写进 API schema——OpenAPI / GraphQL 把 ID 当作独立类型,比指望每个前端都记得 2^53 规则靠谱得多。
还有个 footgun:如果改成字符串 ID,TS 前端把 id 写成 number 会直接在编译期挂掉,这是好的失败方式。静默永远是最贵的那种。
你们在契约层怎么处理 ID 类型,全量字符串还是只过阈值才转?