透過將代表性 C# 類別實例序列化為嚴格 JSON,並把結果貼到 JSON Schema Generator,即可從 C# 類別產生 JSON Schema;該工具會在瀏覽器本地依據 Draft 2020-12 推斷 schema,不依賴反射或執行階段中繼資料。這個工具不會解析 .cs 檔、不讀取屬性 (Attribute),也不會查詢泛型約束條件;它只讀取 JSON 值,並把每個觀察到的基本型別、陣列和物件對應到相應的 schema 關鍵字,因此 C# 類別只是你餵進去之樣本藍圖的角色。產生後,在每個合併物件樣本中都出現的屬性會落入 required 陣列,null 值會把 null 加入 type 聯合型別,混合的數值型別會由 integer 放寬為 number,而觀察到的不相容型別會產生確定性的 anyOf 分支。屬性名稱會被原封不動地保留,包括標點與原型鏈外觀的拼法;至於 additionalProperties,刻意維持不指定,因為單一樣本無法證明未出現的鍵是被禁止的。對一組固定輸入而言輸出是確定性的,因此同一段 JSON 永遠會產生相同的 schema 文字,適合用於審查與版本控管。

從 C# 類別到 JSON Schema 的工作流程
C# 開發者會基於幾種不同的理由採用 JSON Schema:以 OpenAPI 進行契約優先的 API 設計、在 ASP.NET 請求管線中進行執行階段驗證、針對第三方 payload 撰寫整合測試,以及從 schema 向下產生 record 或 DTO 的程式碼。因此,最自然的心智模型是「類別 → schema」,但 JSON Schema Generator 反轉了這個方向,只從 JSON 樣本運作。這種反轉並非要你去對抗的限制,而是最可靠的起點,因為 JSON payload 才是你的服務在線上實際發出或接收內容的真相。像 [Required]、[JsonPropertyName],以及 nullable 引用型別等註解,可能被你的序列化程式遵循,也可能不會,而基於反射的 schema 產生器在不同的 Roslyn 版本之間會產生飄移。相對地,產生的 schema 描述的是線上看到的確切形狀,因此縮短 C# 模型與契約之間的落差,會變成單純地去比對樣本,而不是去審核整個工具鏈的工作。如果你在 Visual Studio 中開發,以樣本在 Visual Studio 設定 JSON Schema 這份配套教學會示範同一份 Draft 2020-12 產出要如何接入 IntelliSense 與編輯器驗證。
產生器如何把 JSON 對應到 C# 形狀的 Schema
推斷過程刻意採用字面意義。JSON 字串、安全整數、其他數值、布林值與 null 都會對應到對應的 JSON Schema 型別;陣列則會根據每個觀察到的元素來推斷其 items。當你把一個 List<int> 序列化為 [1, 2, 3] 時,產生器會看到三個整數,並輸出 type: array 加上 items: { type: integer }。然而,空陣列會產生空白的 items schema,因為從零次觀察中無法正當推論出任何元素約束;這是一個刻意的選擇,目的是把不確定性顯露出來,而不是憑空捏造猜測。下表彙整了產生器在常用 C# 屬性型別,與它從序列化樣本所產生之 schema 關鍵字之間的對應關係。
| C# 屬性型別 | 樣本 JSON 值 | 推斷出的 Schema 關鍵字 |
|---|---|---|
| string | "hello" | type: "string" |
| int (安全) | 42 | type: "integer" |
| long 或大型 decimal | 9999999999 | type: "integer" |
| bool | true | type: "boolean" |
| DateTime (字串形式) | "2025-01-15T00:00:00Z" | type: "string" |
| Nullable<T> 且觀察到 null | null | 把 "null" 加入 type 陣列,並保留 properties 或 items |
| List<int> | [1, 2, 3] | type: "array", items: { type: "integer" } |
| List<T> (空) | [] | type: "array", items: {} |
| 自訂類別 | { "id": 1, "name": "Ada" } | type: "object", properties, required (當存在於每個合併樣本時) |
| List<object> 混合型別 | [1, "two"] | items: { anyOf: [{type: "integer"}, {type: "string"}] } |
巢狀物件與陣列會以遞迴方式推斷,這代表一個包含 Address 類別與 List<Order> 的 Customer 類別,可以一次就產生完整展開的 schema 樹。相容的混合數值 (例如同一陣列中同時有 integer 與 double) 會由 integer 放寬為 number;觀察到的不相容型別則會成為確定性的 anyOf 分支。產生器永遠不會憑空生成「封閉物件」策略,因此最終的 schema 會把未知鍵視為可接受,直到你的權威契約另有聲明為止。
逐步產生 Schema
- 使用 System.Text.Json、Newtonsoft.Json,或你慣用的序列化器,以正式環境的命名政策與 null 處理設定,將代表性 C# 類別實例序列化為嚴格 JSON。
- 把產生的 JSON 貼進 JSON Schema Generator 的輸入區,並視需要在標題欄位輸入簡潔的 schema 標題。
- 點選 generate,並檢視輸出,確認必要欄位、可空屬性、陣列項目、anyOf 分支,以及你在樣本中預期會出現卻可能遺漏的鍵。
- 把產生的 Draft 2020-12 schema 文字複製到你的編輯器、儲存庫,或你慣用的 schema registry。
- 以一款支援 Draft 2020-12 的正式驗證器 (配置與執行階段設定完全相同),同時丟入合法與非法的實例進行測試。
整個流程都在瀏覽器本地執行:你的 JSON 會在當前分頁內被解析,頁面不會上傳資料、不會執行驗證器、也不會連線到任何 schema registry,因此即使在開發階段檢視機密性 payload,也不必離開你的本機。
必要欄位、可空屬性與 anyOf 分支
在以 C# 衍生的 schema 中,有三種模式反覆出現,值得你在信任輸出之前先加以理解。必要欄位僅依「是否出現」而定:在每個合併物件樣本中都觀察到的屬性會落入 required 陣列;在至少一個物件中缺席的屬性則不會列入 required。可空屬性會以 null 與帶型別的值並存的形式出現,這會讓 null 加入 type 陣列,同時保留原本的 properties 或 items schema。例如,一個序列化為 "hello" 的 C# string? 仍維持 type: string,而序列化為 null 的則會變成 type: ["string", "null"]。當產生器在相同陣列位置或合併物件位置觀察到不相容型別時,就會出現 anyOf 分支;重複且等價的分支會被移除,使輸出維持可審閱性。透過抽象基底類別或介面實作的 C# 多型,常會以 anyOf 的形式在此浮現,因為每個具體型別會產生不同的觀察形狀,而合併器並無從得知哪個子型別才是正統。
產生器拒絕推斷的項目
這是基於結構化樣本的推論,而非 schema 探索。產生器不會去猜測 format、pattern、enum、const、minimum、maximum、multipleOf、長度、唯一性、描述、預設值、範例、棄用標記、readOnly、writeOnly、內容編碼、參考、錨點、ID、條件式邏輯、unevaluated properties 或商業語意。以上每一個關鍵字都必須從權威需求或領域文件中手動加入。對語意約束而言,少量樣本不足以作為證據:無法從一個值推斷出 enum、無法從一個字串推斷出 regex、無法從一個數字推斷出範圍。相同的道理也說明了 additionalProperties 為何缺席:單一樣本無法證明未觀察到的鍵是被禁止的,因此所產生的物件會保持開放,直到你的契約明確將它封閉。請手動加入這些關鍵字,然後再以你實際部署到正式環境的 Draft 2020-12 相容驗證器重新驗證,因為依你選用的詞彙表與驗證器設定,format 可能僅為註解性質。在 https://json-schema.org/draft/2020-12 的方言 URI 設定了契約本身,但每個函式庫對邊緣案例關鍵字的解讀都略有不同。
限制、錯誤與驗證器的選擇
產生器強制設定四項硬性上限:500,000 個輸入字元、50,000 個值、40 層巢狀深度,以及 1,000,000 個輸出字元。突破邊界時不會輸出局部 schema,這代表格式錯誤或過大的輸入會產生明確的錯誤訊息,而不是悄悄截斷、你可能不小心出貨的契約。整個流程都套用嚴格 JSON 解析規則:註解、尾端逗號、NaN、Infinity、undefined、BigInt 以及 JavaScript 字面值全部會失敗;JavaScript 會在推斷前先解析樣本數字,因此只有安全整數會成為 type integer。對精度敏感的 decimal 與識別碼,可能需要以字串形式承載,並搭配應用層級的驗證。一旦複製出 schema,便請以正式環境中實際使用的驗證器、設定與詞彙表,同時測試正向與負向實例。JSON Schema 核心規格所引用的 Draft 2020-12 方言宣告,並不代表每個函式庫都會以完全相同的方式解讀每個關鍵字;因此工具之間的契約飄移是一個真實存在的故障模式,只有靠實際測試才能抓出。
想深入了解,請參閱 Check Generated Rust Structs After JSON Conversion。