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


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

См. также