杂记 › 技术笔记 › JSON 转 7 种语言代码时,8 个键名会被整字段吞掉,null 字段照样算必填

JSON 转 7 种语言代码时,8 个键名会被整字段吞掉,null 字段照样算必填

给 CI 补类型校验时踩到一件怪事:一份 JSON 样例转成七种语言的代码,TS 那份 interface 里凭空少了一个键,而生成过程只挂了一条“同一个对象里有重复键名,取最后一次出现的值”,那两个键毫不重复。

我把 json-to-lang 的生成端往回查了一遍,同一批样本逐档复现,这样的地方不止一处。

一、8 个键名会让字段整个消失

577 行判重用 node.vals[k.v] !== undefined 走原型链,挂在 Object.prototype 上的成员被误判成"已经见过"后丢掉。被吞的键名共 8 个:constructor、__proto__、toString、valueOf、hasOwnProperty、isPrototypeOf、propertyIsEnumerable、toLocaleString。

输入样本TS 生成体提示
{"toString":1,"keep":2}只剩 keep: number1 条"重复键名"
{"toString":1}空的 Root 接口2 条
{"foo":1,"keep":2}两个字段齐全0 条

字段一个不剩时 types 仍记 1,提示只带键路径(形如 $.a),没有第几次出现的位置。

二、null 字段不是一律标可空

可空记号只在 null 与非 null 同位置共现时出现。[{"a":null},{"a":1}] 出 TS number | null、C# int?、Go *int、Python Optional[int]、Dart int?;单值 {"a":null} 出 TS a: any、Go interface{}、Python Any,七档 0 条提示。首屏示例的 note 与 address.zip 双双落进 required,Address 必填 ["city", "zip"]。

三、大整数的末位在解析阶段就没了

{"a":9007199254740993} 解析后是 9007199254740992;Java 与 C# 给 long、Go 给 int64、TS 仍是 number 并附 1 条"整数超出双精度安全范围"。668 行的判据是严格大于 9007199254740991,等于边界的样本类型相同却 0 条警告,产物里从不出现数字字面量。

数值类型只看写法不看大小:2147483647 全给 int,2147483648 与 -2147483649 才升 long 或 int64(913 行)。1e2 与 1.0 因带 e 或小数点被判浮点,数值相等的 100 却给 int。1e400 不报错,parseFloat 得 Infinity,只过 553 行的 isNaN 检查,照旧出 double 与 float64,0 条警告。

四、键名会被改写,中文键偏偏不改

清洗链入口是 438 到 462 行的 jlIdentFor:hello world 在 TS 里是 helloWorld,C# 与 Go 是 HelloWorld,Python 是 hello_world。中文键看 415 行的 jlIsIdent,它允许 charCode 大于 127 起步,"用户名"在 TS、Java、C#、Python、Dart 五档原样保留,只有 Go 加前缀成 X用户名,Schema 档不改。

"保留原始键名"只有 TS 一档有(152 到 158 行,19 个控件里唯一带键名策略的),其余各档:Java 用 @JsonProperty 并按 1236 行补 import,C# 写 [JsonPropertyName("hello world")],Python 只有 pydantic 模式给 Field(alias=…),dataclass 不勾"输出注释"就完全没有原名(1401 行)。

五、拆类后的类名不带父类前缀

6 层嵌套不拼根类型名:类名从最内层往外是 L5 到 L1 再加 Root,types 记 6。

各档行数 TypeScript 24 Go 26 Python 32 C# 42 Schema 47 Java 80 Dart 108

图 1:6 层嵌套样本在七档上的行数。

数组元素的类名走 471 到 484 行的 jlSingularize 单数化,Categories 变 Category,Bysis 不动;两个元素的键互不重叠时,并集出的 2 个字段全部可选并各带 1 条提示。空数组和空对象退化成任意类型,TS 拿 any[],警告在 682 与 700 行。

六、提示要过两道上限

收集上限 237 行的 JL_WARN_CAP=400,644 行攒够就直接 return 静默丢弃;展示上限 JL_MAX_WARNINGS=40(235 与 1887 行),界面只画前 40 条。

提示条数 上限 60 元素 60 条 420 元素 400 条 画出的 40 条 未显示

图 2:提示量与可见量,绿段是界面上的 40 条。

60 元素的交替异型数组 warnings=60,界面显示 40 条加"另有 20 条提示未显示。";420 个元素时 warnings 先被截到 400(lines=427、types=2),末尾写"另有 360 条提示未显示。"。

七、左侧三个统计都不是字节数

上方三列来自 1878 到 1880 行:text.length 数字符,split 数行数,大小是 jlHumanSize(text.length),236 行的 JL_BIG_INPUT=2097152 比的也是字符数。中文键样本 59 字符、93 字节,界面显示"大小:59 B",比字节数少 34——按字符讲 2 MB 上限并不成立。"格式化"与"压缩"(1976 到 1977 行)是 JSON.parse 再 JSON.stringify,不保留原文,1e400 直接变 null。

常见问题

Q:C# 档的类型形态能换吗?

不能,只出 public class。types=4 的样本里四个类全是 class,C# 分组唯一的控件是"输出 using 文件头"(171 到 172 行),模板全文搜不到 record 分支。

Q:金额字段选了"精确小数"就稳了吗?

只真正作用于 3 个目标:Java 出 BigDecimal、C# 出 decimal、Python 出 Decimal,TS 仍 number、Dart 仍 double、Schema 仍 integer;Go 补 1 条 1690 行的提示,标准库没有精确小数类型。

Q:能把多条响应拼在一起推断吗?

可以,这是让可选与可空显出来的唯一路径:623 行的判定只接受 1 个 JSON 值,多条样本要自己拼成数组。

那个消失的字段不是推断缺陷,只是键名撞上了原型链上的一个成员。我把产物当草稿而不是契约,字段名、可空性、数值类型人工过一遍,只把 TS 那份直接进仓库,其余先按生成端与源数据的对应关系核一遍名。

📎 相关文章

后缀决定 MIME:同一份 70 字节 PNG 头改出 18 个后缀、13 种类型和 5 个未知 2026-10-23 转 JPG 时透明被强制填底色:91 个质量档位在 PNG 面前不参与编码 2026-10-23 「全部清除」后还剩 553 字节,「只去掉位置信息」连缩略图都没动 2026-10-23 序号位数只是下限、重名时 (2) 插在后缀之前:批量重命名 2000 个文件的实测口径 2026-10-23 证书指纹实测:1159 字节 DER 与 3 种摘要、4 种写法 2026-10-23