使用.Net6中的System.Text.Json遇到幾個常見問題及解決方案

程式設計實驗室發表於2022-03-23

前言

以前.NetCore是不內建JSON庫的,所以大家都用Newtonsoft的JSON庫,而且也確實挺好用的,不過既然官方出了標準庫,那更方便更值得我們多用用,至少不用每次都nuget安裝Newtonsoft.Json庫了。

不過日常開發使用中會有一些問題,本文記錄一下解決方法,歡迎交流~

(文章末尾包含小彩蛋)

字元編碼問題

預設的 System.Text.Json 序列化的時候會把所有的非 ASCII 的字元進行轉義,這就會導致很多時候我們的一些非 ASCII 的字元就會變成 \uxxxx 這樣的形式,很多場景下並不太友好,我們可以配置字元編碼來解決被轉義的問題。

例子:

var testObj=new {
  Name = "測試",
  Value = 123
};

var json = JsonSerializer.Serialize(testObj);
Console.WriteLine(json);

輸出

{"Name":"\u6D4B\u8BD5","Value":123}

在我們序列化的時候,可以指定一個 JsonSerializeOptions,而這個 JsonSerializeOptions 中有一個 Encoder 我們可以用來配置支援的字元編碼,不支援的就會被轉義,而預設只支援 ASCII 字元。

所以解決方法如下:

var json = JsonSerializer.Serialize(testObj, new JsonSerializerOptions()
{
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
})
Console.WriteLine(json);

輸出結果

{"Name":"測試","Value":123}

字元轉義問題

對於一些包含 html 標籤的文字即使指定了所有字符集也會被轉義,這是出於安全考慮。如果覺得不需要轉義也可以配置,配置使用 JavaScriptEncoder.UnsafeRelaxedJsonEscaping 即可。

示例程式碼

var testObj = new {
    Name = "測試",
    Value = 123,
    Code = "<p>test</p>"
};

var json = JsonSerializer.Serialize(testObj, new JsonSerializerOptions {
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
});
Console.WriteLine(json);

輸出

{"Name":"測試","Value":123,"Code":"\u003Cp\u003Etest\u003C/p\u003E"}

可以看到HTML程式碼被轉義了,這很明顯就不行

解決方法

var json = JsonSerializer.Serialize(testObj, new JsonSerializerOptions {
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});

輸出結果

{"Name":"測試","Value":123,"Code":"<p>test</p>"}

搞定!

物件套娃遞迴問題

這個問題在我之前的一篇文章中有詳細說到:Asp-Net-Core開發筆記:介面返回json物件出現套娃遞迴問題

當時我是用Newtonsoft.Json來解決的,不過當我把這篇文章釋出到部落格園之後,有大佬指出.NetCore標準庫System.Text.Json中也有解決這個問題的方法,於是我這裡也來記錄一下~

首先建立幾個實體類

internal class EntityBase {
    public string Id { get; set; }
}
internal class CrawlTask : EntityBase {
    /// <summary>
    /// 爬蟲名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 建立這個爬蟲的使用者
    /// </summary>
    public User User { get; set; }

    /// <summary>
    /// 使用者ID
    /// </summary>
    public string? UserId { get; set; }
}
internal class User : EntityBase {
    /// <summary>
    /// 使用者名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 使用者建立的爬蟲
    /// </summary>
    public List<CrawlTask> CrawlTasks { get; set; }
}

然後用模擬資料來重現問題

//模擬資料
var crawlTask = new CrawlTask { Name = "爬蟲名稱", UserId= "0f3d4b2f-3b4e-4d08-8f4c-0009a316f041" };
var user = new User { Name = "使用者名稱", CrawlTasks = new List<CrawlTask> { crawlTask } };
crawlTask.User = user;

// 輸出
var json2 = JsonSerializer.Serialize(crawlTask);
Console.WriteLine(json2);

輸出結果,直接報錯

Unhandled exception. System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger tha
n the maximum allowed depth of 64. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.User.CrawlTasks.User.CrawlTasks.U
ser.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.Us
er.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.User.CrawlTasks.Name.
...

我們都知道了這是物件的套娃遞迴問題了

所以接下來直接上解決方法

var json2 = JsonSerializer.Serialize(crawlTask,new JsonSerializerOptions {
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    WriteIndented = true,
    ReferenceHandler = ReferenceHandler.IgnoreCycles
});
Console.WriteLine(json2);

ReferenceHandler.IgnoreCycles方式是.Net6新增加的,可以實現和Newtonsoft.JsonReferenceLoopHandling.Ignore差不多的效果。

最終輸出效果如下

{
  "Name": "爬蟲名稱",
  "User": {
    "Name": "使用者名稱",
    "CrawlTasks": [
      null
    ],
    "Id": null
  },
  "UserId": "0f3d4b2f-3b4e-4d08-8f4c-0009a316f041",
  "Id": null
}

可以看到導致套娃遞迴的屬性變成了null

不過這個和Newtonsoft.Json實現的效果還是有點差異的

在我之前的文章裡,Newtonsoft.Json實現的效果是

{
    "name": "test crawl123",
    "user": {
        "name": "string",
        "crawlTasks": null,
        "id": "0f3d4b2f-3b4e-4d08-8f4c-0009a316f041"
    },
    "userId": "0f3d4b2f-3b4e-4d08-8f4c-0009a316f041",
    "id": "4d52d83b-f3ec-47c6-ab26-e241c09c14d1"
}

可以看到的是,crawlTask.user.crawlTasks這個屬性有差別,System.Text.Json是一個陣列,然後裡面有一個null物件,而Newtonsoft.Json是把這個屬性直接置為null

相比之下,我更喜歡Newtonsoft.Json的實現,因為在前端解析的時候可以很清晰的得到一個空物件,而不是裝著空物件的陣列(有點繞口……

後記

說實話,JSON處理還是Python這類動態語言比較方便

像上面那些問題,Python加個ensure_ascii引數就行(雖然C#也不難)

比如

import json

test_obj = {
    "name": "測試",
    "value": 123,
    "code": "<p>test</p>"
}
print(json.dumps(test_obj, ensure_ascii=False))

有時我還喜歡加個indent引數,這樣輸出來的JSON字串更好看

json.dumps(test_obj, ensure_ascii=False, indent=2)

輸出結果

{
  "Name": "測試",
  "Value": 123,
  "Code": "<p>test</p>"
}

相關文章