本文內容來自我寫的開源電子書《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.Json
的JsonDocument
類。
格式化輸出
想要格式化輸出,需要先把字串轉變成一個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值的型別
可以使用JsonElement
的ValueKind
屬性來獲取值型別:
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;
}
如何在 JsonDocument
和 JsonElement
中搜尋子元素
對 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 國際許可協議進行許可。