從單一 JSON 範例產生的 Zod schema 並不能證明 API 合約——它只是記錄了某個值碰巧呈現的內容。要證明一份合約,必須證明每個有效回應都符合已記錄的規則,且每個無效回應都會被拒絕,這就需要比單一承載更廣泛的測試資料。JSON to Zod Schema Converter 會讀取一個有效的 JSON 值、走訪其結構,並輸出無相依套件的 Zod 原始碼,讓審閱者可以把推斷出的形狀與具權威性的文件進行比對。必填欄位的推斷來自「在每個取樣物件中都出現」,可空值的推斷來自「在另一個型別之外同時觀察到 null」,陣列元素的推斷則來自「在既定預算內觀察到的每個元素」。這些規則都與「API 強制要求此欄位」並不等同。範例中出現的欄位在正式環境仍可能是選填,範例中沒出現的值仍可能完全合法,任何字串格式、數值範圍、列舉、文字值、可辨別聯合型別或範例未編碼進來的商業規則,依然屬於未知。請將輸出視為一份審閱用的初稿,再依據文件、額外的測試資料,以及在你自己專案中釘選(pin)的 Zod 版本加以收斂,最終才能取代真正的合約。

「證明合約」實際上需要什麼
一份經過驗證的合約具備三項特性,而單一步驟的推斷無法達成。它必須為每個變體定義所有必填欄位、拒絕所有違反規則的形狀,並在伺服器實際輸出的承載上保持穩定。要建立這一切,你至少需要:
- 一份具權威性的規格——例如 OpenAPI、JSON Schema、Protobuf 或手寫的合約註記——用以描述已記錄的規則。
- 一套具代表性的測試資料,涵蓋正常路徑、選填欄位的缺席、可空值以及已知的拒絕案例。
- 一份根據已釘選版本的 schema 函式庫所編譯而成的驗證器,並對綠燈與紅燈的測試資料進行演練。
範例推斷只能給你第一份草稿。剩下的就是必須對照真實文件(而不是你碰巧貼上的那段 JSON)進行的驗證工作。產生器對此說得很明確:它的輸出描述的是「觀察到了什麼」,必填狀態、格式、範圍、列舉與商業規則都必須對照具權威性的文件和更廣泛的測試資料進行核對,之後該 schema 才能用於驗證真正的合約。
轉換器從單一 JSON 範例中觀察到什麼
產生器會把每個遇到的 JSON 值對應到一份有文件記載的 Zod 建構函式,並把結果輸出為可讀的原始碼。有限的數字會變成 z.number()、字串變成 z.string()、布林值變成 z.boolean(),而 JSON 的 null 則會變成 z.null()。物件會變成 z.object({...}) 呼叫,其屬性順序依照鍵首次出現的順序。已經是合法 JavaScript 識別字的鍵會保持不加引號;包含空格、連字號、數字開頭、引號以及其他不安全的命名時,則會以 JSON 字串跳脫方式輸出,以確保產生的形狀仍是合法的原始碼。
對於陣列,此工具會走訪輸入預算內的每一個元素。同質的樣本會產生單一元素的 schema;混合的樣本(例如一個同時包含字串、數字和物件的陣列)則會產生一個具確定性的 z.union([...])。若觀察到的元素型別之一是 null,則聯合型別的其他成員會被包進 .nullable(),而不是另外發出一個 z.null() 的分支。巢狀的陣列與物件會以遞迴方式處理;當陣列內含物件時,這些物件之間的鍵會被合併。
這個合併規則正是「合約 vs 樣本」之間落差出現的地方。在每個取樣物件中都出現的屬性會維持必填;在至少一個物件中缺席的屬性則會變成選填,因為樣本無法證明它必定存在。同一個鍵名下觀察到的值會以遞迴方式合併,因此若某個欄位有時裝的是字串、有時裝的是物件,該屬性下就會形成一個聯合型別。對同一段 JSON 反覆進行轉換會產生穩定且具確定性的原始碼,因為聯合型別會被去重並以一致的方式排序。
如何從一個樣本產生可供審閱的 Zod Schema
- 挑選一個有效且具代表性的 JSON 值——例如典型的正常路徑回應,或是用來涵蓋你關注變體的最小測試資料——將其貼進轉換器,並提供一個合法的 JavaScript 識別字作為匯出 schema 的名稱。
- 執行產生器並仔細閱讀輸出的原始碼。逐一檢查每個 .optional()、.nullable()、聯合型別的分支、數值的選項,以及任何來自空陣列的 z.array(z.unknown()),因為這些位置代表樣本提供的證據不足以讓產生器更為具體。
- 把產生的原始碼複製到一個已安裝 Zod 並釘選到你預計出貨版本的專案中,然後編譯該檔案。轉換器不會替你安裝或執行 Zod;這一步必須在你自己的程式碼庫中完成。
- 針對具權威性的資料合約測試所產生的 schema:解析 API 文件、對涵蓋正式環境中通過與被拒案例的代表性測試資料執行驗證,並把 schema 未能捕捉到的每一條規則記錄下來。
- 以人工方式補上缺少的規則——格式、範圍、列舉、整數約束、可辨別聯合型別、預設值、細煉(refinements)——只能以文件或正式環境的測試資料作為來源。不要為了填補樣本實際上未證明的缺口而憑空捏造規則。
產生器刻意拒絕猜測的限制
樣本推斷對一份小型 JSON 承載「無法」確立哪些事情抱持誠實的態度。下列項目工具都不會進行推斷,因為拼字上的差異與一次性樣本無法證明意圖:
- 字串格式——UUID、電子郵件地址、URL、日期、ISO 時間戳記。
- 數值約束——最小值、最大值、僅限整數的旗標、排除性範圍。
- 列舉、文字值聯合型別,以及帶標籤鍵的可辨別聯合型別。
- 預設值、轉換、強制轉型、細煉,以及 brand 型別。
- Record、元組、strict-object passthrough 規則,以及遞迴參照回 schema 本身的參考。
- 以跨欄位謂詞或情境式不變式表達的商業規則。
數字已經經過 JSON.parse,因此遵循 JavaScript 的數字精確度;對於需要嚴格十進位語意的識別字或貨幣值,通常應該保留為字串,而非轉換為 z.number()。JavaScript 註解、尾端逗號、NaN、Infinity、undefined、BigInt 語法、單引號字串,以及裸物件實字會被解析器拒絕,而不是悄悄地被修復。當請求超出其資源上限時,產生器同樣會回傳明確的錯誤:500,000 個輸入字元、50,000 個走訪過的值、40 層巢狀深度,或 1,000,000 個產生的字元。一旦觸發上限便會回傳錯誤,絕不會輸出半完成的 schema。
| 轉換器推斷的內容 | 你必須自行補上的內容 |
|---|---|
| 基本型別(字串、有限數字、布林值、null) | 字串格式(UUID、URL、電子郵件、日期) |
| 物件形狀與來源順序的鍵 | 列舉與文字值聯合型別 |
| 巢狀陣列與遞迴合併的物件 | 數值範圍與整數約束 |
| 在至少一個樣本中缺席時視為選填欄位 | 帶標籤鍵的可辨別聯合型別 |
| null 與其他型別同時觀察到時的可空欄位 | 預設值、轉換、細煉、brand |
| 非空陣列的同質與混合元素 schema | 元組、Record、遞迴參照、strict 物件 |
| 空陣列的 z.array(z.unknown()) | 商業規則與跨欄位謂詞 |
把推斷出來的 Schema 化為真正的合約測試
轉換器的輸出是一份起點草稿,而不是最終成品。要把它轉化為能夠證明你合約的東西,你需要一套工作流程:以推斷出的形狀作為鷹架,以具權威性的文件作為真實來源。一個實用的順序看起來像這樣:
- 打開官方 API 規格或合約註解,列出每個必填欄位、允許的值、格式與範圍,並對照推斷出的 schema 把每一項標記為「已捕捉」或「缺少」。
- 以 .refine()、.regex()、z.enum()、z.literal() 或數值的 .min()/.max()/.int() 等呼叫補上缺少的規則。Zod API 文件 列出了可用的建構函式;請挑選與你釘選版本相符的那一份。
- 建立一套測試資料套件,至少為每個已記錄的變體納入一個通過的承載,並為每條已記錄的規則納入一個被拒的承載。對每份測試資料以 safeParse 執行該 schema,並斷言其結果。
- 把這套測試資料的執行串接進你的 CI 流程,使合約的變更必須在同一個 commit 中同時更新 schema 與測試。
遵循這個循環,可以把從樣本衍生出的草稿轉化為一份會被持續維護的合約工件。若缺少對文件的檢視以及測試資料的驗證,推斷出來的 schema 只能證明「某個 JSON 值裝了什麼」——這對審閱固然有用,但本身絕對不足以獨當一面。
依賴推斷時常見的錯誤
有兩個陷阱出現的頻率高到值得特別提出。第一個是把單一擷取到的回應當成完整的合約;正式環境的 API 會新增欄位、在特定條件下省略欄位,並對同一個端點接受多種形狀。第二個是把推斷出來的 schema 當成「測試資料產生器」而不是「對真實資料的檢查工具」——兩個方向都有用處,但 schema 的職責是拒絕無效的形狀,而不是憑空生成它們。把推斷出的原始碼讀成「對某一次觀察的描述」,而不是「對 API 的定義」,正是讓這套工作流程保持誠實的習慣。
若想更深入了解,請參閱 PX to REM Without an API: A Local Browser Workflow。
若想更深入了解,請參閱 Decode ULID Timestamps in Bulk Without an API。