雜記 › 技術筆記 › 把 JSON 轉成 7 種語言的型別程式碼時,8 個原型鏈鍵名會讓整個欄位消失,null 欄位照樣算必填

把 JSON 轉成 7 種語言的型別程式碼時,8 個原型鏈鍵名會讓整個欄位消失,null 欄位照樣算必填

給 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: 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 那份直接進倉庫,其餘先按生成端與原始資料的對應關係核一遍名。

📎 相關文章

70 個位元組沒動,MIME 類型換了 13 次:副檔名才是唯一輸入 2026-10-23 轉 JPG 時透明被強制填底色:91 個品質檔位在 PNG 面前不參與編碼 2026-10-23 「全部清除」後還剩 553 位元組,「只去掉位置信息」連縮圖都沒動 2026-10-23 序號位數只是下限、重名時 (2) 插在後綴之前:批次重新命名 2000 個檔案的實測口徑 2026-10-23 兩條憑證指紋一條 95 個字元一條 64 個:差的 31 個全是冒號,摘要物件是整張 DER 2026-10-23