Использование Utf8JsonReader в System.Text.Json
В этой статье показано, как использовать Utf8JsonReader тип для создания пользовательских анализаторов и десериализаторов.
Utf8JsonReader — это высокопроизводительное, низкое выделение, средство чтения только для пересылки для текста JSON в кодировке UTF-8. Текст считывается из или 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 строки типа.
Подсчитывает объекты и значения свойств name, заканчивающиеся на University.
Предполагается, что файл кодируется как UTF-16 и перекодируется в UTF-8.
Файл, закодированный как UTF-8, можно считывать непосредственно в файл
ReadOnlySpan<byte>
с помощью следующего кода:ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
Если файл содержит метку порядка байтов (BOM) UTF-8, удалите ее перед передачей байтов в
Utf8JsonReader
, так как средство чтения ждет текст. В противном случае метка порядка байтов считается недопустимым кодом JSON, и средство чтения создает исключение.
Ниже приведен пример JSON, который можно считать с помощью приведенного выше кода. Полученным итоговым сообщением будет 2 out of 4 have names that end with University (2 из 4 имеют имена, заканчивающиеся на 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"
}
]
Совет
Версию примера с асинхронными операциями см. в проекте JSON с примерами для .NET.
Чтение из потока с помощью Utf8JsonReader
При чтении большого файла (гигабайта или большего размера, например), может потребоваться избежать загрузки всего файла в память одновременно. В таких ситуациях можно использовать FileStream.
При использовании Utf8JsonReader
для чтения из потока данных действуют следующие правила:
- Буфер, который накапливает часть данных в формате JSON, должен быть не меньше самого крупного из возможных маркеров JSON, чтобы средство чтения могло продвигаться вперед.
- Размер буфера должен быть не меньше самой большой последовательности пробелов в JSON.
- Средство чтения не запоминает прочитанные данные, пока не дойдет до следующего свойства TokenType в структуре JSON. Таким образом, если в буфере еще остались байты, их нужно снова передать в средство чтения. Для определения количества оставшихся байтов можно использовать BytesConsumed.
В примере кода ниже показано, как выполнять чтение из потока. Этот пример демонстрирует MemoryStream. Аналогичный код будет работать и с FileStream, за исключением случаев, когда в начале FileStream
содержится метка порядка байтов UTF-8. В этом случае необходимо удалить эти три байта из буфера, прежде чем передавать оставшиеся байты в Utf8JsonReader
. В противном случае средство чтения создаст исключение, поскольку метка порядка байтов не считается допустимой частью JSON.
Пример кода начинается с буфера 4 КБ и увеличивает размер буфера каждый раз при обнаружении того, что размер недостаточно велик, чтобы соответствовать полному токену JSON, который требуется для чтения, чтобы выполнить вперед ход выполнения полезных данных JSON. Представленный в этом примере фрагмент JSON приводит к увеличению размера буфера только в том случае, если задан очень маленький начальный размер буфера, например 10 байт. Если указать для буфера начальное значение 10, то инструкции Console.WriteLine
продемонстрируют причину и последствия увеличения размера буфера. При начальном размере буфера 4 КБ весь пример JSON отображается каждым вызовом Console.WriteLine
, и размер буфера никогда не требуется увеличивать.
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 ГБ, поскольку удвоение размера 1 ГБ приводит к переполнению буфера int32
.
Ограничения структуры ссылок
Utf8JsonReader
Поскольку тип является типомref struct
, он имеет определенные ограничения. Например, его нельзя хранить в виде поля в классе или структуре, отличной от структуры ref struct
.
Чтобы обеспечить высокую ref struct
производительность, необходимо иметь значение, Utf8JsonReader
так как он должен кэшировать входной байт> ReadOnlySpan<(сам по себе ).ref struct
Кроме того, тип изменяется, Utf8JsonReader
так как он содержит состояние. Поэтому передайте его по ссылке, а не по значению. Utf8JsonReader
Передача по значению приведет к копированию структуры, и изменения состояния не будут видны вызывающей функции.
Дополнительные сведения об использовании структур ссылок см. в разделе "Избегание выделения".
Чтение текста UTF-8
Для достижения максимальной производительности при использовании Utf8JsonReader
полезные данные JSON уже закодированы как текст UTF-8, а не как строки UTF-16. Пример кода см. в разделе Фильтрация данных с помощью Utf8JsonReader.
Чтение с помощью ReadOnlySequence с несколькими сегментами
Если входные данные JSON являются байтами >ReadOnlySpan<, каждый элемент JSON можно получить из ValueSpan
свойства средства чтения при прохождении цикла чтения. Однако если входные данные являются байтами > ReadOnlySequence<(что является результатом чтения из 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, JsonTypeInfo<TValue>, Boolean, CancellationToken) перегрузкуDeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken). По умолчанию DeserializeAsyncEnumerable
пытается передавать элементы, содержащиеся в одном массиве JSON верхнего уровня. Передайте true
параметр для потоковой передачи topLevelValues
нескольких значений верхнего уровня.
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
Встроенные API System.Text.Json
возвращают только типы значений, не допускающие значения NULL. Например, Utf8JsonReader.GetBoolean возвращает bool
. Он вызывает исключение, если обнаруживает Null
в JSON. В следующих примерах показаны два способа обработки значений 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 можно скопировать неискаченную строку в буфер, который вы владеете. В следующем фрагменте кода показан пример использования строки UTF-16 с помощью CopyString.
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. Экземпляр можно преобразовать
Utf8JsonReader
вJsonNode
вызываемый JsonNode.Parse(Utf8JsonReader, Nullable<JsonNodeOptions>). Фрагмент кода приведен ниже.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 предоставляет возможность создания DOM только для чтения с помощью
Utf8JsonReader
. JsonDocument.ParseValue(Utf8JsonReader) Вызовите метод для синтаксического анализаJsonDocument
из экземпляраUtf8JsonReader
. Вы можете получить доступ к элементам JSON, составляющим полезные данные с помощью JsonElement типа. Пример кода, который используется JsonDocument.ParseValue(Utf8JsonReader), см. в разделе RoundtripDataTable.cs и фрагмент кода в десериализации выводимых типов в свойствах объектов.Кроме того, можно проанализировать
Utf8JsonReader
экземпляр с JsonElementопределенным значением JSON, вызвав вызов JsonElement.ParseValue(Utf8JsonReader).