2026年11月11日 星期三

[.NET11 新功能] .NET 11 System.Text.Json:聯集、封閉類別與 AOT 支援

[.NET11 新功能] .NET 11 System.Text.Json:聯集、封閉類別與 AOT 支援

摘要:整理 .NET 11 System.Text.Json 的聯集與封閉類別支援、來源產生器改進及新增的 JSON 註記功能。



內容:


Microsoft .NET Libraries 團隊的首席工程師 Eric 介紹了 .NET 11 中 System.Text.Json(STJ)的更新。今年的重點包括支援聯集型別與封閉類別、改善來源產生器和原生 AOT 的使用體驗、提供更具表達力的型別註記,以及修正可靠性問題。團隊共合併 60 個 PR,其中 17 個涉及新功能或 API、16 個改善來源產生器與 AOT、8 個改善效能、19 個修正錯誤。


聯集型別的序列化與反序列化


STJ 能辨識聯集型別,並直接依照目前聯集成員的型別輸出 JSON。例如,一個可容納整數、字串或一般物件的聯集,序列化後分別會得到 JSON 數字、字串或物件;不會額外加入包裝結構或型別識別欄位。


反序列化時,STJ 通常根據 JSON 值的類型選擇聯集成員。因此,包含數字、字串與物件的 JSON 陣列,可以反序列化成對應的聯集陣列。但這也帶來限制:如果多個聯集成員都對應同一種 JSON 類型,STJ 就無法僅憑 JSON 值判斷目標型別。例如 DateOnly 與 Guid 都會表示為 JSON 字串;由兩者組成的聯集可以正常序列化,反序列化時卻可能因目標型別不明確而失敗。


STJ 採取這種保守的型別推斷方式,是為了避免不受限制的多型反序列化所帶來的安全問題。若需要處理同一 JSON 類型對應多個 .NET 型別的情況,可以註冊型別分類器,讓程式檢查 JSON 內容,再決定要繫結至哪個型別。例如,分類器可以嘗試將字串解析為日期或 GUID,並據此選擇聯集成員;若兩者都不符合,則回報錯誤。


對於由不同物件組成的聯集,STJ 也提供內建的結構分類方式。以 Dog 與 Cat 兩種物件為例,若它們具有足以區分彼此的屬性,反序列化時就能依 JSON 中出現的屬性選擇型別。這種分類只看屬性結構,不會深入分析屬性值,因此較為脆弱:一旦兩種物件日後新增相同屬性,原本可區分的結構就可能變得模糊。


封閉類別與多型 JSON 契約


講者接著介紹封閉類別。過去若要序列化多型的事件模型,通常必須為每個衍生型別明確加入 JsonDerivedType 等註記,並設定型別識別值。一般類別的繼承階層是開放的,編譯時無法得知所有可能的衍生型別,因此需要逐一宣告。


封閉類別的衍生型別則限定於目前的編譯範圍,編譯器可以提供完整清單。STJ 因而能在啟用封閉型別多型推斷後,自動取得這些衍生型別,減少隨模型擴大而增加的註記程式碼。


若能自行決定 JSON 契約的設計,講者建議優先使用封閉類別階層來表達多種選擇。封閉類別同樣能提供窮舉處理的保證,且可使用型別識別欄位,使結構在日後擴充時較為穩定。聯集型別不使用這類識別欄位;當它必須依內容或屬性結構判別型別時,契約演進便較容易受到影響。若既有結構無法更改,例如必須與 TypeScript 風格的聯集資料互通,聯集仍是合適選擇。


來源產生器與原生 AOT 改進


.NET 11 縮小了 STJ 反射式序列化與來源產生器之間的功能差距。過去,透過 JsonConstructor 或 JsonInclude 存取私有成員的情境,來源產生器未能完整支援;現在可在較新的執行環境利用 unsafe accessors 存取,在較舊環境則透過相容方式處理,同時兼顧 AOT 安全性。


init 屬性的指派行為也獲得改善。過去即使 JSON 中沒有對應屬性,來源產生器仍可能指派預設值,導致原有資料被覆蓋;現在可以依欄位是否出現在 JSON 中,有條件地進行指派。此外,JSON 序列化內容類別可宣告為 partial,方便將較大的內容定義拆分至多個檔案。


另一項重要改進是泛型轉換器註記。以往泛型物件若需要自訂 JSON 轉換器,往往還得撰寫使用反射的 JsonConverterFactory;這不但程式碼繁複,也不利於原生 AOT。新版允許在轉換器註記中使用開放泛型宣告,由來源產生器補入對應的泛型參數。使用時,轉換器的泛型參數必須與目標型別相符。


更具表達力的 JSON 註記


.NET 11 新增可套用於型別的命名原則註記,使特定型別的 JSON 屬性名稱不再完全取決於執行時的 JsonSerializerOptions。例如,可以要求某個型別的屬性一律使用 camelCase,同時針對個別屬性覆寫為 kebab-case。這有助於讓 JSON 契約不受外部選項變動影響。


原本主要用於個別屬性的忽略條件註記,也擴充至型別層級。若希望某個型別的所有 null 屬性都不輸出,便不必在每個屬性上重複加入相同註記。


其他更新與使用建議


其他更新包括:新增 JSON Lines 序列化支援,以補齊既有的反序列化能力;加入 F# 辨識式聯集的內建支援,但講者提醒聯集支援目前使用反射,尚不支援來源產生器;支援泛型多型階層及相關的開放泛型註記;新增 PascalCase 命名原則,以及支援 .NET 11 新增的數值與集合型別。


整體而言,這次更新一方面讓 STJ 配合新的語言型別模型,另一方面著重減少來源產生器與 AOT 情境中的限制。實際設計資料模型時,若 JSON 結構由自己掌控,應優先考慮可使用型別識別欄位的封閉階層;若必須遵循既有、沒有識別欄位的 JSON 結構,再選用聯集,並留意反序列化時的型別歧義。

沒有留言:

張貼留言