给 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: number | 1 条"重复键名" |
{"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。
图 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 条。
图 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 那份直接进仓库,其余先按生成端与源数据的对应关系核一遍名。