如何在 System.Text.Json 中使用 Utf8JsonReader
本文介绍如何使用 Utf8JsonReader 类型生成自定义分析程序和反序列化程序。
Utf8JsonReader 是面向 UTF-8 编码 JSON 文本的一个高性能、低分配的只进读取器。 从 ReadOnlySpan<byte>
或 ReadOnlySequence<byte>
读取文本。 Utf8JsonReader
是一种低级类型,可用于生成自定义解析器和反序列化程序。 (JsonSerializer.Deserialize 方法在幕后使用 Utf8JsonReader
。)
下面的示例演示如何使用 Utf8JsonReader 类。 此代码假定 jsonUtf8Bytes
变量是一个字节数组,其中包含编码为 UTF-8 的有效 JSON。
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
注意
不能直接从 Visual Basic 代码使用 Utf8JsonReader
。 有关详细信息,请参阅 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”结尾的属性值进行计数。
假设文件编码为 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"
}
]
提示
若要获取此示例的异步版本,请参阅 .NET 示例 JSON 项目。
使用 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
语句会说明缓冲区大小增加的原因和影响。 在初始缓冲区大小为 4KB 的情况下,每次调用 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
之外的类或结构中。
为了实现高性能,Utf8JsonReader
必须是 ref struct
,因为它需要缓存输入的 ReadOnlySpan<byte>(它本身是 ref struct
)。 此外,Utf8JsonReader
类型是可变的,因为它包含状态。 因此,它按引用传递而不是按值传递。 按值传递 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;
// ...
}
}
读取多个 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
当 AllowMultipleValues 被设置为 true
时,还可以从包含无效 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 中转义的任何字符取消转义。 下面的示例演示如何搜索名为“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)
{
// ...
}
相关 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
实例分析为表示特定 JSON 值的 JsonElement。