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


Использование Utf8JsonReader в System.Text.Json

В этой статье показано, как использовать Utf8JsonReader тип для создания пользовательских анализаторов и десериализаторов.

Utf8JsonReader — это последовательный модуль чтения текста JSON в кодировке UTF-8 из 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 является массив байтов, который содержит допустимые данные 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

Версию примера с асинхронными операциями см. в проекте JSON с примерами для .NET.

Предыдущий код:

  • Предполагается, что 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"
  }
]

Чтение из потока с помощью 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, как он должен кэшировать входной байт> ReadOnlySpan<, который сам является структурой ссылок. Кроме того, тип изменяется, 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;
        // ...
    }
}

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

Встроенные 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)
{
    // ...
}

См. также