LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

2025年:是時候重新認識System.Text.Json了

freeflydom
2025年8月1日 8:50 本文熱度 716

曾幾何時,在.NET的世界里,Newtonsoft.Json如同一位德高望重的王者,無人不曉。直到有一天,一位名叫System.Text.Json(后文簡稱STJ)的新貴悄然登場。它出身名門(.NET官方),身懷絕技(號稱性能超群),本應是明日之星,卻被無數開發者貼上了“坑王”、“難用”、“反人類”的標簽。

無數個深夜,開發者們為了解決一個看似簡單的JSON序列化問題,從STJ切換回NSJ,嘴里念叨著:“STJ,勸退了。”然后默默地Install-Package Newtonsoft.Json,仿佛這才是解決問題的唯一“騷操作”。

但是,這一切真的公平嗎?時過境遷,如今.NET 10的預覽版都已發布,STJ早已不是當年的吳下阿蒙。那些曾經讓你抓狂的“坑”,有多少只是因為誤解了它的設計哲學?有多少早已被新版本填平?

今天,就讓我們一起為STJ來一場轟轟烈烈的“正名運動”,讓你徹底告別因它而起的“996”!

告別加班:STJ與Newtonsoft行為對齊實戰

很多時候,我們覺得STJ“不好用”,僅僅是因為它的默認行為和牛頓不一樣。STJ的設計哲學是:性能優先、安全第一、嚴格遵守RFC 8259規范。而牛頓則更傾向于靈活方便、兼容并包。下面我們就通過一個個小故事和代碼示例,看看如何通過簡單的配置,讓STJ的行為像我們熟悉的老朋友牛頓一樣。

1. 大小寫問題:前端傳的name,我C#的Name怎么就收不到了?

背景故事:
小王剛接手一個前后端分離的項目,前端用JS,遵循駝峰命名(camelCase),傳來一個JSON:{"name": "張三", "age": 18}。后端的C#模型用的是帕斯卡命名(PascalCase):public class User { public string Name { get; set; } public int Age { get; set; } }。結果用STJ一反序列化,user.Nameuser.Age全都是null0!小王抓耳撓腮,查了半天才發現是大小寫匹配問題,差點就要加班調試一晚上了。

騷操作揭秘:
STJ為了極致性能,默認是區分大小寫的。而牛頓默認是不區分的。我們只需一個配置項就能解決問題。

using System.Text.Json;
var jsonFromJs = "{\"name\": \"張三\", \"age\": 18}";
// 默認行為,會匹配失敗
var optionsDefault = new JsonSerializerOptions();
var userDefault = JsonSerializer.Deserialize<User>(jsonFromJs, optionsDefault);
Console.WriteLine($"默認行為: Name = {userDefault.Name}"); // 輸出: 默認行為: Name = 
// 騷操作:開啟不區分大小寫匹配
var optionsInsensitive = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};
var userInsensitive = JsonSerializer.Deserialize<User>(jsonFromJs, optionsInsensitive);
Console.WriteLine($"開啟不區分大小寫: Name = {userInsensitive.Name}"); // 輸出: 開啟不區分大小寫: Name = 張三
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

小貼士: 在ASP.NET Core的Web API項目中,默認已經幫你開啟了PropertyNameCaseInsensitive = true,所以你可能根本沒遇到過這個問題,但如果你手動調用JsonSerializer,就需要注意了。

2. 命名策略:我的UserName怎么就不能變成userName

背景故事:
小李的后端API返回的JSON字段都是Pascal風格,比如{"UserName": "Lisi", "IsEnabled": true}。前端小伙伴抱怨說這不符合JS社區的規范,希望能統一用駝峰命名{"userName": "Lisi", "isEnabled": true}。小李心想,難道要把所有C#屬性名都改成小寫開頭?這也太不優雅了!

騷操作揭秘:
當然不用!STJ提供了命名策略(Naming Policy),讓你輕松轉換。

using System.Text.Json;
var user = new User { UserName = "Lisi", IsEnabled = true };
// 默認行為,Pascal風格
var optionsDefault = new JsonSerializerOptions { WriteIndented = true };
var jsonDefault = JsonSerializer.Serialize(user, optionsDefault);
Console.WriteLine("默認輸出:\n" + jsonDefault);
// 默認輸出:
// {
//   "UserName": "Lisi",
//   "IsEnabled": true
// }
// 騷操作:指定駝峰命名策略
var optionsCamelCase = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true
};
var jsonCamelCase = JsonSerializer.Serialize(user, optionsCamelCase);
Console.WriteLine("\n駝峰輸出:\n" + jsonCamelCase);
// 駝峰輸出:
// {
//   "userName": "Lisi",
//   "isEnabled": true
// }
public class User
{
    public string UserName { get; set; }
    public bool IsEnabled { get; set; }
}

小貼士: 同樣,在ASP.NET Core中,默認也幫你配置了駝峰命名策略。這就是為什么你的API天生就符合前端規范。

3. 注釋和尾隨逗號:這JSON怎么就不“合法”了?

背景故事:
老張需要處理一批由其他系統生成的JSON配置文件,這些文件里竟然帶了注釋,而且數組末尾還可能有個多余的逗號,比如 [1, 2, 3, /*這是注釋*/]Newtonsoft.Json處理這些文件毫無壓力,但System.Text.Json一上來就拋出JsonException,直接罷工。

騷操作揭秘:
STJ嚴格遵守RFC 8259規范,該規范不允許注釋和尾隨逗號。但為了兼容性,它也提供了開關。

using System.Text.Json;
// 注意3后面有一個尾隨逗號
var nonStandardJson = @"{
    ""name"": ""帶注釋的JSON"",
    ""data"": [
        1,
        2,
        3,
    ]
}";
// 默認行為,直接拋異常
try
{
    JsonSerializer.Deserialize<object>(nonStandardJson);
}
catch (JsonException ex)
{
    // The JSON array contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 6 | BytePositionInLine: 4.
    Console.WriteLine("默認行為,果然報錯了: " + ex.Message);
}
// 騷操作:允許注釋和尾隨逗號
var tolerantOptions = new JsonSerializerOptions
{
    ReadCommentHandling = JsonCommentHandling.Skip, // 跳過注釋
    AllowTrailingCommas = true // 允許尾隨逗號
};
var deserializedObject = JsonSerializer.Deserialize<object>(nonStandardJson, tolerantOptions);
Console.WriteLine("\n開啟兼容模式后,成功解析!");

4. null值的處理:滿屏的null看著好煩!

背景故事:
小趙的API返回的用戶信息里,有些字段是可選的,比如MiddleName。當這些字段沒有值時,序列化出的JSON里會包含"middleName": null。這不僅增加了網絡傳輸的數據量,前端同學也覺得處理起來很麻煩,他們希望null值的字段干脆就不要出現在JSON里。

騷操作揭秘:
牛頓通過NullValueHandling.Ignore可以輕松實現,STJ同樣可以。

using System.Text.Json;
using System.Text.Json.Serialization;
var user = new User { FirstName = "San", LastName = "Zhang", MiddleName = null };
// 默認行為,包含null值
var optionsDefault = new JsonSerializerOptions { WriteIndented = true };
var jsonDefault = JsonSerializer.Serialize(user, optionsDefault);
Console.WriteLine("默認輸出:\n" + jsonDefault);
// 騷操作:序列化時忽略null值
var optionsIgnoreNull = new JsonSerializerOptions
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    WriteIndented = true
};
var jsonIgnoreNull = JsonSerializer.Serialize(user, optionsIgnoreNull);
Console.WriteLine("\n忽略null值輸出:\n" + jsonIgnoreNull);
public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleName { get; set; }
}

5. 帶引號的數字:"age": "30" 也能是數字?

背景故事:
小孫在對接一個非常“古老”的第三方API,返回的JSON里,所有的數字都是用字符串表示的,例如{"age": "30"}。STJ在反序列化到int Age屬性時直接拋出異常,因為它認為"30"是字符串,不是數字。難道還得先反序列化成string再手動int.Parse

騷操作揭秘:
不用那么麻煩,STJ早就想到了這種不規范但常見的情況。

using System.Text.Json;
using System.Text.Json.Serialization;
var jsonWithQuotedNumber = @"{""Age"": ""30""}";
// 默認行為,拋出異常
try
{
    JsonSerializer.Deserialize<User>(jsonWithQuotedNumber);
}
catch (JsonException ex)
{
    // The JSON value could not be converted to System.Int32. Path: $.Age | LineNumber: 0 | BytePositionInLine: 12.
    Console.WriteLine("默認行為,報錯了: " + ex.Message);
}
// 騷操作:允許從字符串讀取數字
var optionsAllowQuotedNumbers = new JsonSerializerOptions
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};
var user = JsonSerializer.Deserialize<User>(jsonWithQuotedNumber, optionsAllowQuotedNumbers);
Console.WriteLine($"\n開啟兼容模式后,Age = {user.Age}");
public class User
{
    public int Age { get; set; }
}

6. 循環引用:我和我的老板,誰先序列化?

背景故事:
小錢在使用Entity Framework時,遇到了經典難題:Employee對象有個Manager屬性,Manager對象又有個DirectReports列表包含了這個Employee。一序列化,就陷入了“你中有我,我中有你”的無限循環,最終JsonException爆棧。

騷操作揭秘:
這是STJ在.NET 5和.NET 6中重點解決的問題。現在我們有兩種選擇。

using System.Text.Json;
using System.Text.Json.Serialization;
var manager = new Employee { Name = "老板" };
var employee = new Employee { Name = "小錢", Manager = manager };
manager.DirectReports = new List<Employee> { employee };
// 默認行為,拋出循環引用異常
try
{
    JsonSerializer.Serialize(employee);
}
catch (JsonException ex)
{
    // A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 64. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. 
    // Path: $.Manager.DirectReports.Manager.DirectReports.Manager.DirectReports.……
    Console.WriteLine("默認行為,循環引用報錯: " + ex.Message);
}
// 騷操作:忽略循環引用點(推薦用于API)
var optionsIgnoreCycles = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.IgnoreCycles,
    WriteIndented = true
};
var jsonIgnoreCycles = JsonSerializer.Serialize(employee, optionsIgnoreCycles);
Console.WriteLine("\n忽略循環引用輸出:\n" + jsonIgnoreCycles);
// 輸出中,老板的DirectReports里的小錢的Manager屬性會是null
public class Employee
{
    public string Name { get; set; }
    public Employee Manager { get; set; }
    public List<Employee> DirectReports { get; set; }
}

小貼士: 還有一個ReferenceHandler.Preserve選項,它會通過$id$ref元數據來完整保留對象圖,適合需要完美往返(round-trip)序列化的場景,但生成的JSON通用性較差。對于Web API,IgnoreCycles通常是更好的選擇。

7. 枚舉變字符串:別再給我返回01了!

背景故事:
小周的API里有個Gender枚舉,序列化后默認變成了數字01。前端每次都要查文檔才知道0Male1Female。這溝通成本也太高了!

騷操作揭秘:
一個轉換器就能搞定,讓你的枚舉變得可讀。

using System.Text.Json;
using System.Text.Json.Serialization;
var user = new User { Gender = Gender.Male };
// 默認行為,序列化為數字
var optionsDefault = new JsonSerializerOptions { WriteIndented = true };
var jsonDefault = JsonSerializer.Serialize(user, optionsDefault);
Console.WriteLine("默認輸出:\n" + jsonDefault);
// {
//   "Gender": 0
// }
// 騷操作:添加枚舉字符串轉換器
var optionsEnumAsString = new JsonSerializerOptions
{
    Converters = { new JsonStringEnumConverter() },
    WriteIndented = true
};
var jsonEnumAsString = JsonSerializer.Serialize(user, optionsEnumAsString);
Console.WriteLine("\n枚舉轉字符串輸出:\n" + jsonEnumAsString);
// 枚舉轉字符串輸出:
// {
//   "Gender": "Male"
// }
public class User
{
    public Gender Gender { get; set; }
}
public enum Gender { Male, Female }

8. 讓JSON回歸人類可讀:與中文和AI友好相處

背景故事: 我興沖沖地序列化了一個包含中文的對象,準備發給新接入的AI大模型。結果一看日志,"騷操作" 變成了 "\u9A9A\u64CD\u4F5C"!我當時就懵了,這不僅我看著費勁,AI能看懂嗎?Token數暴增暫且不說,理解上出現偏差怎么辦?難道又要退回Newtonsoft?

騷操作揭秘: 這可能是對STJ誤解最深的一點。STJ默認這樣做,是出于極致的安全考慮。它的默認編碼器JavaScriptEncoder.Default會轉義所有非ASCII字符以及HTML敏感字符(如<>&),這是為了防止當你的JSON被不當地嵌入到HTML <script>標簽中時,引發XSS(跨站腳本)攻擊。它遵循的是“默認安全”的最高原則。

然而,在如今API交互的時代,我們通常通過Content-Type: application/json來通信,數據并不會直接嵌入HTML。特別是在與大模型(LLM)交互時,保持中文字符的原樣不僅能讓我們人類更容易閱讀和調試,更能讓AI準確無誤地理解語義,同時顯著減少Token消耗。這時,我們可以明確地告訴STJ:“我清楚我的使用環境是安全的,請別轉義!”

// 默認行為:輸出Unicode轉義字符,安全至上
var escapedJson = JsonSerializer.Serialize("騷操作");
// escapedJson -> "\u9A9A\u64CD\u4F5C"
// 騷操作配置:在安全的環境下,讓JSON對人類和AI更友好
var options = new JsonSerializerOptions()
{
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var readableJson = JsonSerializer.Serialize("騷操作", options);
// readableJson -> "騷操作"

你看,STJ不是“坑”,它只是個安全感爆棚、需要你明確指令的“直男”而已。

9. JsonDocument 難用?你可能找錯了“對標對象”!

背景故事: 小鄭需要動態地構建一個復雜的JSON對象,或者在反序列化后對JSON結構進行一些增刪改查。他懷念著Newtonsoft.JsonJObjectJArray的靈活與強大,于是他找到了STJ里的JsonDocument。結果他驚奇地發現,這玩意兒居然是只讀的!“這怎么用?連個屬性都不能改,簡直是反人類設計!” 小鄭抱怨道,差點就把STJ拉進了黑名單。

騷操作揭秘: 這是一個經典的“指鹿為馬”式誤解。JsonDocument的設計目標是對標高性能的只讀文檔模型,它的核心優勢是低內存占用極速查詢,它通過Utf8JsonReader直接操作原始的UTF-8字節流,避免了將整個JSON字符串物化為.NET對象,因此性能極佳。但它的使命是“讀”,而不是“寫”。

真正的JObject/JArray對標物,是.NET 6中隆重推出的**JsonNode及其派生類JsonObjectJsonArray!這是一個可變的、功能完善的文檔對象模型(DOM)**,它提供了你所期望的一切動態操作能力。

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Encodings.Web;
// 騷操作:使用JsonNode動態構建和修改JSON
Console.WriteLine("--- 使用JsonNode ---");
// 1. 創建一個JsonObject,就像 new JObject()
var rootNode = new JsonObject();
// 2. 像字典一樣輕松添加屬性
rootNode["message"] = "Hello, JsonNode!";
rootNode["user"] = new JsonObject
{
    ["name"] = "小鄭",
    ["isActive"] = true
};
// 3. 添加一個JsonArray
var scores = new JsonArray(88, 95, 100);
rootNode["scores"] = scores;
Console.WriteLine("添加屬性后的JSON:\n" + rootNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
// 4. 輕松修改屬性值
rootNode["user"]["isActive"] = false;
Console.WriteLine("\n修改屬性后的JSON:\n" + rootNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
// 5. 序列化為字符串,完美兼容中文
var finalOptions = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
var finalJson = rootNode.ToJsonString(finalOptions);
Console.WriteLine("\n最終輸出(兼容中文):\n" + finalJson);

所以,下次當你想動態操作JSON時,請記住口訣:要讀就用JsonDocument,要寫就用JsonNode

快速參考:行為對齊配置映射表

為了方便大家快速查找,我把上面的騷操作整理成了一個表格:

Newtonsoft.Json 行為System.Text.Json 默認行為System.Text.Json 配置 (JsonSerializerOptions)備注
反序列化大小寫不敏感區分大小寫PropertyNameCaseInsensitive = trueASP.NET Core默認已開啟此選項。
駝峰命名策略PascalCase (與C#屬性名一致)PropertyNamingPolicy = JsonNamingPolicy.CamelCaseASP.NET Core默認已開啟此選項。
忽略JSON注釋拋出異常ReadCommentHandling = JsonCommentHandling.Skip
忽略尾隨逗號拋出異常AllowTrailingCommas = true
序列化時忽略null值包含null值DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
反序列化帶引號的數字拋出異常NumberHandling = JsonNumberHandling.AllowReadingFromString例如,將"123"反序列化為int類型。
處理循環引用拋出異常ReferenceHandler = ReferenceHandler.IgnoreCyclesIgnoreCycles將循環點置為null,是API場景的常用選擇。
枚舉序列化為字符串序列化為數字添加 new JsonStringEnumConverter() 到 Converters 集合Newtonsoft.Json中也有類似的StringEnumConverter
中文字符轉義轉義非ASCII字符Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping適用于API和AI交互,減少Token消耗。
動態操作JSON (JObject)JsonDocument (只讀)使用 JsonNode / JsonObject / JsonArray.NET 6及以后版本可用,功能對標JObject

STJ的進化之路:從追趕者到引領者

通過上面的例子,我們看到大部分所謂的“坑”其實都是可以通過JsonSerializerOptions輕松配置的。但我們也要承認,STJ在早期版本中確實存在一些功能上的“天坑”。幸運的是,微軟的開發團隊堪稱“基建狂魔”,他們用一個個版本的迭代,不僅填平了所有坑,更鋪就了一條通往未來的高速公路。

讓我們沿著時間的脈絡,回顧STJ這場精彩的逆襲之戰:

.NET 5 - 奠定基礎,補齊核心短板

這是STJ發布后的第一個“大招版本”,一口氣解決了從Newtonsoft.Json遷移過來的最大痛點,讓STJ在許多真實場景中變得“堪用”。

  • 循環引用處理: 引入ReferenceHandler.Preserve,讓處理EF Core等復雜對象圖不再是噩夢。
  • 非公共成員支持: 帶來了[JsonInclude][JsonConstructor]特性,終于可以序列化私有屬性和使用私有構造函數了,解救了無數依賴封裝性的開發者。
  • 非字符串鍵字典: 開始原生支持Dictionary<int, T>這類常見結構,不再拋出惱人的NotSupportedException

.NET 6 - 性能飛躍,擁抱現代范式

如果說.NET 5讓STJ“能用”,那么.NET 6則讓它真正“好用”和“快用”,并為未來的AOT(預編譯)時代打下堅實基礎。

  • 源碼生成器 (JsonSerializerContext): 這是STJ的“核武器”!通過在編譯時生成無反射的序列化代碼,極大地提升了性能、降低了內存占用,并成為Blazor Wasm AOT和Native AOT等場景下的唯一選擇。
  • 可變DOM (JsonNode): 提供了官方的JObject/JArray替代品,讓動態解析和構建復雜的JSON對象變得簡單而類型安全。

.NET 7 - 功能完備,實現高級定制

這個版本標志著STJ在功能上基本追平了Newtonsoft.Json,尤其是在高級和復雜的定制場景下,給了開發者充足的信心。

  • 安全的多態序列化: 推出基于[JsonPolymorphic][JsonDerivedType]的白名單式多態支持,功能強大且從設計上根除了Newtonsoft.JsonTypeNameHandling的安全隱患。
  • 終極定制武器 (IJsonTypeInfoResolver): 引入了對標IContractResolver的接口,允許開發者在運行時動態修改類型的序列化“契約”,實現了諸如動態添加/刪除屬性、實現復雜條件序列化等高級“騷操作”。

.NET 8 - 精益求精,優化開發體驗

在功能已經非常完善的基礎上,.NET 8更側重于對現有功能的打磨和對開發者體驗的優化。

  • 源碼生成器增強: 完美支持C# 11的requiredinit屬性,讓不可變模型和源碼生成器能更好地協同工作。
  • 內置更多常用類型支持: 如HalfInt128UInt128等,并改進了對接口層次結構的支持。

.NET 9 - 生態集成,引領行業標準

從.NET 9開始,我們能清晰地看到STJ已經不再滿足于追趕,而是開始作為.NET生態的核心組件,主動引領和定義標準。

  • JSON Schema導出器 (JsonSchemaExporter): 這是一項里程碑式的更新!現在可以直接從你的C#類型生成標準的JSON Schema文檔。這意味著與OpenAPI、Swagger等API工具鏈的集成將變得空前簡單,自動化客戶端生成、API文檔和數據驗證都將因此受益。
  • 更靈活的格式化選項: 允許自定義縮進字符和大小,滿足各種代碼風格和顯示需求。
  • 更強的類型安全: 默認將更嚴格地遵守C#的可空引用類型注解,進一步減少運行時錯誤。

綜上所述,System.Text.Json的進化之路,是一部清晰的從滿足基本需求,到追求極致性能,再到實現功能完備,并最終引領生態發展的成長史。那些關于它的陳舊“坑論”,早已被滾滾向前的版本車輪碾得粉碎。

?轉自https://www.cnblogs.com/sdcb/p/19010852


該文章在 2025/8/1 8:50:40 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
色久综合网精品一区二区 | 日本午夜两性视屏 | 亚洲人成电影在线观看影院 | 亚洲最大在线观看AV网站 | 亚洲女同精品一区二区视频 | 真实国产乱子伦视频 |