.NET 5的System.Text.Json的JsonDocument類講解

sogeisetsu發表於2021-12-02

本文內容來自我寫的開源電子書《WoW C#》,現在正在編寫中,可以去WOW-Csharp/學習路徑總結.md at master · sogeisetsu/WOW-Csharp (github.com)來檢視編寫進度。預計2021年年底會完成編寫,2022年2月之前會完成所有的校對和轉制電子書工作,爭取能夠在2022年將此書上架亞馬遜。編寫此書的目的是因為目前.NET市場相對低迷,很多優秀的書都是基於.NET framework框架編寫的,與現在的.NET 6相差太大,正規的.NET 5學習教程現在幾乎只有MSDN,可是MSDN雖然準確優美但是太過瑣碎,沒有過閱讀開發文件的同學容易一頭霧水,於是,我就編寫了基於.NET 5的《WoW C#》。本人水平有限,歡迎大家去本書的開源倉庫sogeisetsu/WOW-Csharp關注、批評、建議和指導。

解析JSON字串

注意本文重點講解的是解析,而非序列化,關於序列化請檢視WOW-Csharp/陣列和集合.md at master · sogeisetsu/WOW-Csharp (github.com)

c#解析json字串在.NET 5的首選是使用System.Text.JsonJsonDocument類。

格式化輸出

想要格式化輸出,需要先把字串轉變成一個JsonDocument例項化物件,然後在序列化這個物件的時候指定JsonSerializerOptions為整齊列印。

// 先定義一個json字串
string jsonText = "{\"ClassName\":\"Science\",\"Final\":true,\"Semester\":\"2019-01-01\",\"Students\":[{\"Name\":\"John\",\"Grade\":94.3},{\"Name\":\"James\",\"Grade\":81.0},{\"Name\":\"Julia\",\"Grade\":91.9},{\"Name\":\"Jessica\",\"Grade\":72.4},{\"Name\":\"Johnathan\"}],\"Teacher'sName\":\"Jane\"}";
Console.WriteLine(jsonText);
// 將表示單個 JSON 字串值的文字分析為 JsonDocument
JsonDocument jsonDocument = JsonDocument.Parse(jsonText);
// 序列化
string formatJson = JsonSerializer.Serialize(jsonDocument, new JsonSerializerOptions()
                                             {
                                                 // 整齊列印
                                                 WriteIndented = true,
                                                 Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
                                             });
// 格式化輸出
Console.WriteLine(formatJson);

這個比較麻煩,我們可以將其製作成擴充方法。

internal static class JsonDocumentExtensions
{
    internal static string JDFormatToString(this JsonDocument jsonDocument)
    {
        return JsonSerializer.Serialize(jsonDocument, new JsonSerializerOptions()
                                        {
                                            WriteIndented = true,
                                            Encoder=JavaScriptEncoder.Create(UnicodeRanges.All)
                                        });
    }

    internal static string TOJsonString(this string str)
    {
        JsonDocument jsonDocument = JsonDocument.Parse(str);
        return JsonSerializer.Serialize(jsonDocument, new JsonSerializerOptions()
                                        {
                                            WriteIndented = true,
                                            Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
                                        });
    }
}

這樣就可以用類似於下面的方法直接呼叫了:

// jsondocument 格式化輸出為json字串
string a = jsonDocument.JDFormatToString();
// 格式化字串
string b = jsonText.TOJsonString();

JSON DOM

對JSON進行DOM操作.NET提供了兩種官方方法,分別是JsonDocumenth和JSonNode,其中JsonNode提供了建立可變 DOM 的能力,它更加強大和簡單,但是JsonNode是.NET 6的內容,鑑於.NET 6的穩定版剛剛釋出,所以本文還是講解JsonDocumenth。.NET 6是一個LTS版本,它於2021年11月8日正式釋出,會支援到2024年11月8日,詳情可以檢視.NET and .NET Core official support policy (microsoft.com)筆者會在.NET 5結束支援之前(2022 年 5 月 8 日)寫一篇關於JSonNode的文章作為本文的Patch。

JsonDocument 提供了使用 Utf8JsonReader 構建只讀 DOM 的能力。可以通過 JsonElement 型別訪問組成有效負載的 JSON 元素。 JsonElement 型別提供陣列和物件列舉器以及用於將 JSON 文字轉換為常見 .NET 型別的 API。 JsonDocument 公開一個 RootElement 屬性。

關於JsonDocument,MSDN上有一篇非常好的講解文章How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json | Microsoft Docs,這一部分筆者更多的是採用MSDN上面的例子,此部分在某種意義上可以看作對How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json的翻譯。

用作例子的Json資料如下:

{
    "Class Name": "Science",
    "Teacher's Name": "Jane",
    "Semester": "2019-01-01",
    "Students": [
        {
            "Name": "John",
            "Grade": 94.3
        },
        {
            "Name": "James",
            "Grade": 81
        },
        {
            "Name": "Julia",
            "Grade": 91.9
        },
        {
            "Name": "Jessica",
            "Grade": 72.4
        },
        {
            "Name": "Johnathan"
        }
    ],
    "Final": true
}

方法概述

先將其反序列化成JsonDocument物件:

Console.WriteLine("對json字串進行dom操作");
string jsonText = "{\"ClassName\":\"Science\",\"Final\":true,\"Semester\":\"2019-01-01\",\"Students\":[{\"Name\":\"John\",\"Grade\":94.3},{\"Name\":\"James\",\"Grade\":81.0},{\"Name\":\"Julia\",\"Grade\":91.9},{\"Name\":\"Jessica\",\"Grade\":72.4},{\"Name\":\"Johnathan\"}],\"Teacher'sName\":\"Jane\"}";
JsonDocument jsonDocument = JsonDocument.Parse(jsonText);

獲取當前JsonDocument的根元素(JsonElement型別):

JsonElement root = jsonDocument.RootElement;

RootElement是json資料的根,後續所有的操作都與其息息相關。

GetProperty根據鍵名,獲取根元素下的元素(JsonElement型別):

JsonElement students = root.GetProperty("Students");

GetArrayLength獲取陣列屬性的長度(如果將此方法用於非陣列型別的json值會報錯):

// 獲取陣列長度
Console.WriteLine(students.GetArrayLength());

可以對值型別為陣列的JsonElement使用EnumerateArray ()方法來獲取列舉器(IEnumerator),從而進行迴圈操作:

// EnumerateArray 一個列舉器,它用於列舉由該 JsonElement 表示的 JSON 陣列中的值。
foreach (JsonElement student in students.EnumerateArray())
{
    Console.WriteLine(student);
    Console.WriteLine(student.ValueKind);// object
    // 獲取屬性Name的string值
    Console.WriteLine(student.GetProperty("Name").GetString());
}

獲取值

對於JsonElement獲取元素值的方式比較複雜,首先需要知道值的型別,然後根據值的型別來選擇方法,方法列表可以從JsonElement 結構 (System.Text.Json) | Microsoft Docs檢視,比如值的型別是double,就使用GetDouble()來獲取json數字(double型別):

Console.WriteLine(student.GetProperty("Grade").GetDouble());

如果當前的值是string型別,就使用GetString()來獲取json字串:

Console.WriteLine(semester.GetString());

總之,為了獲取準確的json值,必須提前知道json值的型別。這樣才會最大限度保證不會出錯。

獲取和判讀Json值的型別

可以使用JsonElementValueKind屬性來獲取值型別:

Console.WriteLine(students.ValueKind);

ValueKind屬性的型別是名為JsonValueKind的列舉型別。JsonValueKind的欄位如下:

Array 2 JSON 陣列。
False 6 JSON 值 false
Null 7 JSON 值 null
Number 4 JSON 數字。
Object 1 JSON 物件。
String 3 JSON 字串。
True 5 JSON 值 true
Undefined 0 沒有值(不同於 Null)。

故可以使用像下面這種判斷相等的方式來檢測資料型別:

Console.WriteLine(students.ValueKind == JsonValueKind.Array); // true

檢查屬性是否存在

可以使用TryGetProperty方法來根據鍵來判斷元素是否存在,demo如下:

root.TryGetProperty("Name", out JsonElement value)

存在就返回true,不存在就返回false,TryGetProperty的第一個引數是鍵名,第二個引數是用out關鍵字修飾的JsonElement型別,如果此屬性存在,會將其值分配給 value 引數。

藉助TryGetProperty既可以判斷屬性是否存在,也能在屬性存在的情況下獲取該屬性對應的JsonElement,demo:

// 檢查存在
Console.WriteLine(root.TryGetProperty("Semester", out JsonElement value));
// 使用被分配的JsonElement
Console.WriteLine(value.GetString());

MSDN的demo更具備實用性:

if (student.TryGetProperty("Grade", out JsonElement gradeElement))
{
    sum += gradeElement.GetDouble();
}
else
{
    sum += 70;
}

如何在 JsonDocumentJsonElement 中搜尋子元素

對 JsonElement 的搜尋需要對屬性進行順序搜尋,因此速度相對較慢(例如在使用 TryGetProperty 時)。 System.Text.Json 旨在最小化初始解析時間而不是查詢時間。因此,在搜尋 JsonDocument 物件時使用以下方法來優化效能:

  • 使用內建的列舉器(EnumerateArray 和 EnumerateObject)而不是自己做索引或迴圈。不要對陣列形式的JsonElement進行諸如students[1]的操作。
  • 不要使用 RootElement 通過每個屬性對整個 JsonDocument 進行順序搜尋。相反,根據 JSON 資料的已知結構搜尋巢狀的 JSON 物件。也就是說不要進行不必要的搜尋,要根據自己對所操作的JSON的最大瞭解程度來進行搜尋,比如明知道某個json陣列裡面沒有自己想要的資料,就別去對它進行一遍又一遍的搜尋

JsonDocument 是非託管資源

因為是非託管資源,其不會在CLR中被託管。JsonDocument 型別實現了 IDisposable ,為了記憶體的整潔應該在using塊中使用,使其在使用完之後立即被釋放資源,就像下面這樣:

using (JsonDocument jsonDocument = JsonDocument.Parse(jsonText))
{
    // 對jsonDocument的各種操作
}

前面筆者沒有將其放在using塊裡面純粹是為了演示方便,請大家注意在using塊中使用JsonDocument 才是推薦的用法。

函式呼叫JsonDocument

如果因為某種原因需要將JsonDocument轉給方法呼叫者,則僅從您的 API 返回 JsonDocument,在大多數情況下,這不是必需的。返回返回 RootElement 的 Clone就好,它是一個 JsonElement。demo如下:

public JsonElement LookAndLoad(JsonElement source)
{
    string json = File.ReadAllText(source.GetProperty("fileName").GetString());

    using (JsonDocument doc = JsonDocument.Parse(json))
    {
        return doc.RootElement.Clone();
    }
}

如果你所編寫的方法收到一個 JsonElement 並返回一個子元素,則沒有必要返回子元素的克隆。呼叫者負責使傳入的 JsonElement 所屬的 JsonDocument 保持活動狀態即可。demo如下:

public JsonElement ReturnFileName(JsonElement source)
{
   return source.GetProperty("fileName");
}

LICENSE

已將所有引用其他文章之內容清楚明白地標註,其他部分皆為作者勞動成果。對作者勞動成果做以下宣告:

copyright © 2021 蘇月晟,版權所有。

知識共享許可協議
作品蘇月晟採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

相關文章