Поделиться через


Использование 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);
    

    Если файл содержит метку порядка байтов UTF-8 (BOM), удалите ее перед передачей этих байтов в Utf8JsonReader, поскольку читатель ожидает текст. В противном случае метка порядка байтов (BOM) считается недопустимым кодом JSON, и средство чтения выбрасывает исключение.

Ниже приведен пример JSON, который можно считать с помощью приведенного выше кода. Итоговое сообщение: "2 из 4 имеют имена, заканчивающиеся на 'Университет'".

[
  {
    "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 содержится BOM (метка порядка байтов) UTF-8. В этом случае необходимо удалить эти три байта из буфера, прежде чем передавать оставшиеся байты в Utf8JsonReader. В противном случае чтение выбросило бы исключение, поскольку BOM не считается допустимой частью 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.

Ограничения ref struct

Utf8JsonReader Поскольку это тип ref struct, он имеет определенные ограничения. Например, его нельзя хранить в виде поля в классе или структуре, отличной от структуры ref struct.

Чтобы обеспечить высокую Utf8JsonReader производительность, ref struct должен быть ref struct, так как необходимо кэшировать входной элемент ReadOnlySpan<byte> (который сам по себе является ref struct). Кроме того, Utf8JsonReader тип является изменяемым, так как он содержит состояние. Поэтому передайте его по ссылке, а не по значению. Передача Utf8JsonReader по значению приведет к копированию структуры, и изменения состояния не будут видны вызывающей стороне.

Дополнительные сведения об использовании ref-структур см. в разделе "Избежание выделения памяти".

Чтение текста UTF-8

Для достижения максимальной производительности при использовании Utf8JsonReader читайте полезные данные JSON, уже закодированные как текст UTF-8, а не как строки UTF-16. Пример кода см. в разделе Фильтрация данных с помощью Utf8JsonReader.

Чтение с помощью ReadOnlySequence с несколькими сегментами

Если ваши входные данные JSON представлены в виде ReadOnlySpan<byte>, каждый элемент JSON можно получить через свойство ValueSpan средства чтения по мере прохождения цикла чтения. Однако, если ваши входные данные — это 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 верхнего уровня. Передайте 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, и другой путем возврата значения по умолчанию.

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

См. также