如何在 System.Text.Json 中使用 Utf8JsonReader

本文示範如何使用 Utf8JsonReader 類型來建置自訂剖析器和還原序列化程式。

Utf8JsonReader 是一個高效能、低配置、只能順向讀取的 UTF-8 編碼 JSON 文字讀取器,其會從 ReadOnlySpan<byte>ReadOnlySequence<byte> 開始讀取。 Utf8JsonReader 是一個低階類型,可用於建置自訂剖析器與還原序列化程式。 JsonSerializer.Deserialize 方法在涵蓋範圍下使用 Utf8JsonReader

Utf8JsonReader 無法直接從 Visual Basic 程式碼使用。 如需詳細資訊,請參閱 Visual Basic 支援

下列範例將示範如何使用 Utf8JsonReader 類別:

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

上述程式碼假設 jsonUtf8 變數是位元組陣列,其中包含編碼為 UTF-8 的有效 JSON。

使用 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

如需此範例的非同步版本,請參閱 .NET 範例 JSON 專案

上述 程式碼:

  • 假設 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"
  }
]

使用讀取 Utf8JsonReader 串流

讀取大型檔案 (例如 GB 或更大的大小時),建議您避免一次將整個檔案載入記憶體。 針對此案例,您可以使用 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 結構,所以具有特定限制。 例如,其無法儲存為 ref 結構以外類別或結構上的欄位。

若要達到高效能,此類型必須是 ref struct,因為其需要快取輸入 ReadOnlySpan<byte>,而這本身為 ref 結構。 此外,Utf8JsonReader 類型是可變的,因為其會保留狀態。 因此,請以傳址方式傳遞,而非依值傳遞。 依值傳遞會導致結構複本,且呼叫端看不到狀態變更。

如需如何使用 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;
        // ...
    }
}

針對屬性名稱查閱使用 ValueTextEquals

請勿透過呼叫 SequenceEqual 執行屬性名稱查閱來使用 ValueSpan 執行逐一位元組比較。 因為呼叫 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)
{
    // ...
}

另請參閱