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

本文介绍如何使用 Utf8JsonReader 类型生成自定义分析程序和反序列化程序。

Utf8JsonReader 是面向 UTF-8 编码 JSON 文本的一个高性能、低分配的只进读取器,从 ReadOnlySpan<byte>ReadOnlySequence<byte> 读取信息。 Utf8JsonReader 是一种低级类型,可用于生成自定义分析器和反序列化程序。 JsonSerializer.Deserialize 方法在后台使用 Utf8JsonReader

不能直接从 Visual Basic 代码使用 Utf8JsonReader。 有关详细信息,请参阅 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 变量是包含有效 JSON(编码为 UTF-8)的字节数组。

使用 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”结尾的属性值进行计数。

  • 假设文件编码为 UTF-16,并将它转码为 UTF-8。 可以使用以下代码,将编码为 UTF-8 的文件直接读入 ReadOnlySpan<byte>

    ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
    

    如果文件包含 UTF-8 字节顺序标记 (BOM),请在将字节传递给 Utf8JsonReader 之前将它删除,因为读取器需要文本。 否则,BOM 被视为无效 JSON,读取器将引发异常。

下面是前面的代码可以读取的 JSON 示例。 生成的摘要消息为“2 out of 4 have names that end with '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 从流中读取内容

当读取大型文件(例如,1 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 异常。 如果 JSON 包含大小约为 1 GB 或更大的令牌,则会发生这种情况,因为将 1 GB 大小加倍会导致令牌太大,无法放入 int32 缓冲区。

ref 结构限制

由于 Utf8JsonReader 类型是 ref struct,因此它具有某些限制。 例如,它无法作为字段存储在 ref struct 之外的类或结构中。

若要实现高性能,此类型必须为 ref struct,因为它需要缓存输入 ReadOnlySpan<byte>(这本身便是 ref struct)。 此外,Utf8JsonReader 类型是可变的,因为它包含状态。 因此,它按引用传递而不是按值传递。 按值传递会产生结构副本,状态更改会对调用方不可见。

有关如何使用 ref 结构的详细信息,请参阅避免分配

读取 UTF-8 文本

若要在使用 Utf8JsonReader 时实现可能的最佳性能,请读取已编码为 UTF-8 文本(而不是 UTF-16 字符串)的 JSON 有效负载。 有关代码示例,请参阅使用 Utf8JsonReader 筛选数据

使用多段 ReadOnlySequence 进行读取

如果 JSON 输入是 <>,则在运行读取循环时,可以从读取器上的 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 进行属性名称查找

不要使用 ValueSpan 通过对属性名称查找调用 SequenceEqual 来执行逐字节比较。 改为调用 ValueTextEquals,因为该方法会对在 JSON 中转义的任何字符取消转义。 下面的示例演示如何搜索名为“name”的属性:

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)
{
    // ...
}

另请参阅