如何在 System.Text.Json 中使用 Utf8JsonReader
本文示範如何使用 Utf8JsonReader 類型來建置自訂剖析器和還原序列化程式。
Utf8JsonReader 是UTF-8編碼 JSON 文字的高效能、低配置、僅限轉送讀取器。 文字是從 ReadOnlySpan<byte>
或 ReadOnlySequence<byte>
讀取。 Utf8JsonReader
是一種低階類型,可用來建置自定義剖析器和還原串行化器。 (方法 JsonSerializer.Deserialize 涵蓋範圍下使用 Utf8JsonReader
。
下列範例將示範如何使用 Utf8JsonReader 類別。 此程式代碼假設 jsonUtf8Bytes
變數是位元組數位,其中包含有效的 JSON,編碼為 UTF-8。
var options = new JsonReaderOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
};
var reader = new Utf8JsonReader(jsonUtf8Bytes, options);
while (reader.Read())
{
Console.Write(reader.TokenType);
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
case JsonTokenType.String:
{
string? text = reader.GetString();
Console.Write(" ");
Console.Write(text);
break;
}
case JsonTokenType.Number:
{
int intValue = reader.GetInt32();
Console.Write(" ");
Console.Write(intValue);
break;
}
// Other token types elided for brevity
}
Console.WriteLine();
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support
注意
Utf8JsonReader
無法直接從 Visual Basic 程式碼使用。 如需詳細資訊,請參閱 Visual Basic 支援。
使用 Utf8JsonReader
篩選資料
下列範例示範如何同步讀取檔案並搜尋值。
using System.Text;
using System.Text.Json;
namespace SystemTextJsonSamples
{
public class Utf8ReaderFromFile
{
private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
private static ReadOnlySpan<byte> Utf8Bom => new byte[] { 0xEF, 0xBB, 0xBF };
public static void Run()
{
// ReadAllBytes if the file encoding is UTF-8:
string fileName = "UniversitiesUtf8.json";
ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
// Read past the UTF-8 BOM bytes if a BOM exists.
if (jsonReadOnlySpan.StartsWith(Utf8Bom))
{
jsonReadOnlySpan = jsonReadOnlySpan.Slice(Utf8Bom.Length);
}
// Or read as UTF-16 and transcode to UTF-8 to convert to a ReadOnlySpan<byte>
//string fileName = "Universities.json";
//string jsonString = File.ReadAllText(fileName);
//ReadOnlySpan<byte> jsonReadOnlySpan = Encoding.UTF8.GetBytes(jsonString);
int count = 0;
int total = 0;
var reader = new Utf8JsonReader(jsonReadOnlySpan);
while (reader.Read())
{
JsonTokenType tokenType = reader.TokenType;
switch (tokenType)
{
case JsonTokenType.StartObject:
total++;
break;
case JsonTokenType.PropertyName:
if (reader.ValueTextEquals(s_nameUtf8))
{
// Assume valid JSON, known schema
reader.Read();
if (reader.GetString()!.EndsWith("University"))
{
count++;
}
}
break;
}
}
Console.WriteLine($"{count} out of {total} have names that end with 'University'");
}
}
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support
上述 程式碼:
假設 JSON 包含 物件的陣列,而且每個物件可能包含字串類型的「name」 屬性。
計算以 "University" 結尾的物件和 "name" 屬性值。
假設檔案編碼為 UTF-16,並將其轉碼為 UTF-8。
編碼為 UTF-8 的檔案可以使用下列程式碼直接讀取至
ReadOnlySpan<byte>
:ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
如果檔案包含 UTF-8 位元組順序標記 (BOM),請先將其移除,再將位元組傳遞至
Utf8JsonReader
,因為讀取器預期文字。 否則,BOM 會被視為無效 JSON,而讀取器會擲回例外狀況。
下列是上述程式碼可讀取的 JSON 範例。 所產生摘要訊息為「4 個中有 2 個名稱的結尾為 'University'」:
[
{
"web_pages": [ "https://contoso.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "contoso.edu" ],
"name": "Contoso Community College"
},
{
"web_pages": [ "http://fabrikam.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "fabrikam.edu" ],
"name": "Fabrikam Community College"
},
{
"web_pages": [ "http://www.contosouniversity.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "contosouniversity.edu" ],
"name": "Contoso University"
},
{
"web_pages": [ "http://www.fabrikamuniversity.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "fabrikamuniversity.edu" ],
"name": "Fabrikam University"
}
]
提示
如需此範例的非同步版本,請參閱 .NET 範例 JSON 專案。
使用讀取 Utf8JsonReader
串流
讀取大型檔案時,您可能會想要避免一次將整個檔案載入記憶體。 針對此案例,您可以使用 FileStream。
使用 Utf8JsonReader
讀取串流時,適用下列規則:
- 包含部分 JSON 承載的緩衝區至少必須與其中最大的 JSON 權杖一樣大,以便讀取器可以推展進度。
- 緩衝區必須至少與 JSON 內的最大空白字元序列一樣大。
- 讀取器不會追蹤其讀取的資料,直到其完全讀取 JSON 承載中的下一個 TokenType 為止。 因此,當緩衝區中有剩餘的位元組時,您必須再次將其傳遞至讀取器。 您可以使用 BytesConsumed 來判斷剩餘的位元組數目。
下列程式碼說明如何讀取串流。 此範例顯示 MemoryStream。 類似的程式碼會搭配使用 FileStream,但在 FileStream
的開頭包含 UTF-8 BOM 時除外。 在此情況下,您必須先從緩衝區移除這三個位元組,再將剩餘位元組傳遞至 Utf8JsonReader
。 否則,因為 BOM 不會被視為 JSON 的有效部分,所以讀取器會擲回例外狀況。
範例程式碼會從 4 KB 緩衝區開始,並在每次發現大小不足以符合完整的 JSON 權杖時,將緩衝區的大小加倍,這是讀取器在 JSON 承載上推展進度時的必要作業。 只有在您設定非常小的初始緩衝區大小 (例如 10 個位元組) 時,程式碼片段中提供的 JSON 範例才會觸發緩衝區大小增加。 如果您將初始緩衝區大小設定為 10,則 Console.WriteLine
陳述式會說明緩衝區大小增加的原因和效果。 在 4 KB 的初始緩衝區大小上,每個呼叫 Console.WriteLine
都會顯示整個範例 JSON,而且緩衝區大小永遠不會增加。
using System.Text;
using System.Text.Json;
namespace SystemTextJsonSamples
{
public class Utf8ReaderPartialRead
{
public static void Run()
{
var jsonString = @"{
""Date"": ""2019-08-01T00:00:00-07:00"",
""Temperature"": 25,
""TemperatureRanges"": {
""Cold"": { ""High"": 20, ""Low"": -10 },
""Hot"": { ""High"": 60, ""Low"": 20 }
},
""Summary"": ""Hot"",
}";
byte[] bytes = Encoding.UTF8.GetBytes(jsonString);
var stream = new MemoryStream(bytes);
var buffer = new byte[4096];
// Fill the buffer.
// For this snippet, we're assuming the stream is open and has data.
// If it might be closed or empty, check if the return value is 0.
stream.Read(buffer);
// We set isFinalBlock to false since we expect more data in a subsequent read from the stream.
var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
// Search for "Summary" property name
while (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals("Summary"))
{
if (!reader.Read())
{
// Not enough of the JSON is in the buffer to complete a read.
GetMoreBytesFromStream(stream, ref buffer, ref reader);
}
}
// Found the "Summary" property name.
Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
while (!reader.Read())
{
// Not enough of the JSON is in the buffer to complete a read.
GetMoreBytesFromStream(stream, ref buffer, ref reader);
}
// Display value of Summary property, that is, "Hot".
Console.WriteLine($"Got property value: {reader.GetString()}");
}
private static void GetMoreBytesFromStream(
MemoryStream stream, ref byte[] buffer, ref Utf8JsonReader reader)
{
int bytesRead;
if (reader.BytesConsumed < buffer.Length)
{
ReadOnlySpan<byte> leftover = buffer.AsSpan((int)reader.BytesConsumed);
if (leftover.Length == buffer.Length)
{
Array.Resize(ref buffer, buffer.Length * 2);
Console.WriteLine($"Increased buffer size to {buffer.Length}");
}
leftover.CopyTo(buffer);
bytesRead = stream.Read(buffer.AsSpan(leftover.Length));
}
else
{
bytesRead = stream.Read(buffer);
}
Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState);
}
}
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support
上述範例不會設定緩衝區成長大小的限制。 如果權杖大小過大,則程式碼可能會失敗,並發生 OutOfMemoryException 例外狀況。 因為 1 GB 大小加倍會導致大小過大而無法放入 int32
緩衝區,所以如果 JSON 包含大約 1 GB 或更大的權杖,就會發生這種情況。
ref 結構限制
Utf8JsonReader
因為類型是 ref struct
,所以有某些限制。 例如,它不能儲存為 類別或結構上的欄位,而不是 ref struct
。
若要達到高效能,Utf8JsonReader
必須是 ,因為它需要快取輸入 ReadOnlySpan<位元組>(其本身為 ref struct
)。ref struct
此外,Utf8JsonReader
類型是可變的,因為其會保留狀態。 因此,請以傳址方式傳遞,而非依值傳遞。 傳遞 Utf8JsonReader
by 值會導致結構複本,而且呼叫端看不到狀態變更。
如需如何使用 ref 結構的詳細資訊,請參閱避免配置。
讀取 UTF-8 文字
若要在使用 Utf8JsonReader
時達到最佳效能,請讀取已編碼為 UTF-8 文字的 JSON 承載,而非 UTF-16 字串。 如需程式碼範例,請參閱使用 Utf8JsonReader 篩選資料。
使用多區段 ReadOnlySequence 讀取
如果您的 JSON 輸入是 ReadOnlySpan<byte>,當您執行讀取迴圈時,可以在讀取器上從 ValueSpan
屬性存取每個 JSON 元素。 不過,如果您的輸入是 ReadOnlySequence<byte> (這是從 PipeReader 讀取的結果),則部分 JSON 元素可能會跨越 ReadOnlySequence<byte>
物件的多個區段。 這些元素無法從連續記憶體區塊中的 ValueSpan 存取。 相反地,每當您使用多區段 ReadOnlySequence<byte>
作為輸入時,請輪詢讀取器上的 HasValueSequence 屬性,以找出存取目前 JSON 元素的方法。 下列是建議的模式:
while (reader.Read())
{
switch (reader.TokenType)
{
// ...
ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
reader.ValueSequence.ToArray() :
reader.ValueSpan;
// ...
}
}
讀取多個 JSON 檔
在 .NET 9 和更新版本中,您可以從單一緩衝區或數據流讀取多個空格符分隔的 JSON 檔。 根據預設, Utf8JsonReader
如果偵測到任何追蹤第一個最上層檔的非空格符,則會擲回例外狀況。 不過,您可以使用 旗標來 JsonReaderOptions.AllowMultipleValues 設定該行為。
JsonReaderOptions options = new() { AllowMultipleValues = true };
Utf8JsonReader reader = new("null {} 1 \r\n [1,2,3]"u8, options);
reader.Read();
Console.WriteLine(reader.TokenType); // Null
reader.Read();
Console.WriteLine(reader.TokenType); // StartObject
reader.Skip();
reader.Read();
Console.WriteLine(reader.TokenType); // Number
reader.Read();
Console.WriteLine(reader.TokenType); // StartArray
reader.Skip();
Console.WriteLine(reader.Read()); // False
當 設定為 true
時AllowMultipleValues,您也可以從包含無效 JSON 之尾端數據的承載讀取 JSON。
JsonReaderOptions options = new() { AllowMultipleValues = true };
Utf8JsonReader reader = new("[1,2,3] <NotJson/>"u8, options);
reader.Read();
reader.Skip(); // Succeeds.
reader.Read(); // Throws JsonReaderException.
若要串流多個最上層值,請使用 DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) 或 DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken) 多載。 根據預設, DeserializeAsyncEnumerable
嘗試串流包含在單一最上層 JSON 陣列中的元素。 topLevelValues
傳遞 true
參數以串流多個最上層值。
ReadOnlySpan<byte> utf8Json = """[0] [0,1] [0,1,1] [0,1,1,2] [0,1,1,2,3]"""u8;
using var stream = new MemoryStream(utf8Json.ToArray());
var items = JsonSerializer.DeserializeAsyncEnumerable<int[]>(stream, topLevelValues: true);
await foreach (int[] item in items)
{
Console.WriteLine(item.Length);
}
/* This snippet produces the following output:
*
* 1
* 2
* 3
* 4
* 5
*/
屬性名稱查閱
若要查閱屬性名稱,請勿使用 ValueSpan 呼叫 來執行位元組位元組比較 SequenceEqual。 相反地,請呼叫 ValueTextEquals,因為此方法會取消逸出 JSON 中的任何字元。 下列範例示範如何搜尋名為 "named" 的屬性:
private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
total++;
break;
case JsonTokenType.PropertyName:
if (reader.ValueTextEquals(s_nameUtf8))
{
count++;
}
break;
}
}
將 null 值讀入可為 Null 的實值型別
內建 System.Text.Json
API 只會傳回不可為 Null 的實值型別。 例如,Utf8JsonReader.GetBoolean 會傳回 bool
。 如果在 JSON 中找到 Null
,則會擲回例外狀況。 下列範例示範兩種處理 null 的方法,一種是傳回可為 Null 的實值型別,另一種是傳回預設值:
public bool? ReadAsNullableBoolean()
{
_reader.Read();
if (_reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
{
throw new JsonException();
}
return _reader.GetBoolean();
}
public bool ReadAsBoolean(bool defaultValue)
{
_reader.Read();
if (_reader.TokenType == JsonTokenType.Null)
{
return defaultValue;
}
if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
{
throw new JsonException();
}
return _reader.GetBoolean();
}
跳過權杖的子系
使用 Utf8JsonReader.Skip() 方法跳過目前 JSON 權杖的子系。 如果權杖類型為 JsonTokenType.PropertyName,讀取器會移至屬性值。 下列程式碼片段示範使用 Utf8JsonReader.Skip() 將讀取器移至屬性值的範例。
var weatherForecast = new WeatherForecast
{
Date = DateTime.Parse("2019-08-01"),
TemperatureCelsius = 25,
Summary = "Hot"
};
byte[] jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(weatherForecast);
var reader = new Utf8JsonReader(jsonUtf8Bytes);
int temp;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
{
if (reader.ValueTextEquals("TemperatureCelsius"))
{
reader.Skip();
temp = reader.GetInt32();
Console.WriteLine($"Temperature is {temp} degrees.");
}
continue;
}
default:
continue;
}
}
取用解碼的 JSON 字串
從 .NET 7 開始,您可以使用 Utf8JsonReader.CopyString 方法而非 Utf8JsonReader.GetString() 來取用解碼的 JSON 字串。 不同於 GetString() 一律會配置新字串的,CopyString 可讓您將未逸出的字串複製到您擁有的緩衝區。 下列程式碼片段顯示使用 CopyString 來取用 UTF-16 字串的範例。
var reader = new Utf8JsonReader( /* jsonReadOnlySpan */ );
int valueLength = reader.HasValueSequence
? checked((int)reader.ValueSequence.Length)
: reader.ValueSpan.Length;
char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.AsSpan(0, charsRead);
// Handle the unescaped JSON string.
ParseUnescapedString(source);
ArrayPool<char>.Shared.Return(buffer, clearArray: true);
void ParseUnescapedString(ReadOnlySpan<char> source)
{
// ...
}
相關的 API
若要從
Utf8JsonReader
執行個體還原序列化自訂類型,請呼叫 JsonSerializer.Deserialize<TValue>(Utf8JsonReader, JsonSerializerOptions) 或 JsonSerializer.Deserialize<TValue>(Utf8JsonReader, JsonTypeInfo<TValue>)。 如需範例,請參閱從 UTF-8 還原串行化。JsonNode 和從其衍生的類別提供了建立可變 DOM 的功能。 您可以呼叫 JsonNode.Parse(Utf8JsonReader, Nullable<JsonNodeOptions>),將
Utf8JsonReader
執行個體轉換成JsonNode
。 下列程式碼片段為範例。using System.Text.Json; using System.Text.Json.Nodes; namespace Utf8ReaderToJsonNode { public class WeatherForecast { public DateTimeOffset Date { get; set; } public int TemperatureCelsius { get; set; } public string? Summary { get; set; } } public class Program { public static void Main() { var weatherForecast = new WeatherForecast { Date = DateTime.Parse("2019-08-01"), TemperatureCelsius = 25, Summary = "Hot" }; byte[] jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(weatherForecast); var utf8Reader = new Utf8JsonReader(jsonUtf8Bytes); JsonNode? node = JsonNode.Parse(ref utf8Reader); Console.WriteLine(node); } } }
JsonDocument 提供使用
Utf8JsonReader
建置唯讀 DOM 的功能。 呼叫 JsonDocument.ParseValue(Utf8JsonReader) 方法,從Utf8JsonReader
執行個體剖析JsonDocument
。 您可以透過 JsonElement 類型存取組成承載的 JSON 元素。 如需使用 JsonDocument.ParseValue(Utf8JsonReader) 的範例程式碼,請參閱 RoundtripDataTable.cs 和將推斷類型還原序列化為物件屬性中的程式碼片段。您也可以藉由呼叫 JsonElement.ParseValue(Utf8JsonReader),將
Utf8JsonReader
執行個體剖析為 JsonElement,此執行個體代表特定的 JSON 值。