給 CI 補型別校驗時踩到一件怪事:一份 JSON 樣本轉成七種語言的程式碼,TS 那份 interface 裡憑空少了一個鍵,而生成過程只掛了一條「同一個物件裡有重複鍵名,取最後一次出現的值」,那兩個鍵毫不重複。
我把 JSON 產生程式碼 的生成端往回查了一遍,同一批樣本逐檔復現,這樣的地方不止一處。
一、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 那份直接進倉庫,其餘先按生成端與原始資料的對應關係核一遍名。