Como usar Utf8JsonReader em System.Text.Json

Este artigo mostra como você pode usar o Utf8JsonReader tipo para criar analisadores e desserializadores personalizados.

Utf8JsonReader é um leitor somente para encaminhamento de alto desempenho e baixa alocação para texto JSON codificado em UTF-8. O texto é lido a partir de um ReadOnlySpan<byte> ou ReadOnlySequence<byte>. Utf8JsonReader é um tipo de baixo nível que pode ser usado para criar analisadores e desserializadores personalizados. (Os JsonSerializer.Deserialize métodos utilizados Utf8JsonReader sob as cobertas.)

O exemplo a seguir mostra como usar a Utf8JsonReader classe. Este código assume que a jsonUtf8Bytes variável é uma matriz de bytes que contém JSON válido, codificado como 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://video2.skills-academy.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

Nota

Utf8JsonReader não pode ser usado diretamente do código do Visual Basic. Para obter mais informações, consulte Suporte do Visual Basic.

Filtrar dados usando Utf8JsonReader

O exemplo a seguir mostra como ler um arquivo de forma síncrona e procurar um valor.

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://video2.skills-academy.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

O código anterior:

  • Assume que o JSON contém uma matriz de objetos e cada objeto pode conter uma propriedade "name" do tipo string.

  • Conta objetos e valores de propriedade "name" que terminam com "University".

  • Assume que o arquivo é codificado como UTF-16 e o transcodifica para UTF-8.

    Um arquivo codificado como UTF-8 pode ser lido diretamente em um ReadOnlySpan<byte> usando o seguinte código:

    ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
    

    Se o arquivo contiver uma marca de ordem de bytes (BOM) UTF-8, remova-a antes de passar os bytes para o Utf8JsonReader, pois o leitor espera texto. Caso contrário, a lista técnica é considerada JSON inválida e o leitor lança uma exceção.

Aqui está um exemplo JSON que o código anterior pode ler. A mensagem resumida resultante é "2 em cada 4 têm nomes que terminam com 'Universidade'":

[
  {
    "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"
  }
]

Gorjeta

Para obter uma versão assíncrona deste exemplo, consulte .NET samples JSON project.

Ler a partir de um fluxo usando Utf8JsonReader

Ao ler um arquivo grande (um gigabyte ou mais de tamanho, por exemplo), convém evitar carregar o arquivo inteiro na memória de uma só vez. Para esse cenário, você pode usar um FileStreamarquivo .

Ao usar o para ler de Utf8JsonReader um fluxo, as seguintes regras se aplicam:

  • O buffer que contém a carga JSON parcial deve ser pelo menos tão grande quanto o maior token JSON dentro dele para que o leitor possa progredir.
  • O buffer deve ser pelo menos tão grande quanto a maior sequência de espaço em branco dentro do JSON.
  • O leitor não controla os dados que leu até ler completamente o próximo TokenType na carga JSON útil. Então, quando há bytes restantes no buffer, você tem que passá-los para o leitor novamente. Você pode usar BytesConsumed para determinar quantos bytes sobram.

O código a seguir ilustra como ler a partir de um fluxo. O exemplo mostra um MemoryStreamarquivo . Código semelhante funcionará com um FileStream, exceto quando o FileStream contém uma lista técnica UTF-8 no início. Nesse caso, você precisa remover esses três bytes do buffer antes de passar os bytes restantes para o Utf8JsonReader. Caso contrário, o leitor lançaria uma exceção, uma vez que a lista técnica não é considerada uma parte válida do JSON.

O código de exemplo começa com um buffer de 4 KB e dobra o tamanho do buffer cada vez que descobre que o tamanho não é grande o suficiente para caber um token JSON completo, que é necessário para o leitor avançar na carga JSON útil. O exemplo JSON fornecido no trecho dispara um aumento do tamanho do buffer somente se você definir um tamanho de buffer inicial muito pequeno, por exemplo, 10 bytes. Se você definir o tamanho inicial do buffer como 10, as Console.WriteLine instruções ilustram a causa e o efeito do aumento do tamanho do buffer. No tamanho inicial do buffer de 4 KB, todo o JSON de exemplo é mostrado por cada chamada para Console.WriteLine, e o tamanho do buffer nunca precisa ser aumentado.

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://video2.skills-academy.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

O exemplo anterior não define nenhum limite para o tamanho que o buffer pode crescer. Se o tamanho do token for muito grande, o código poderá falhar com uma OutOfMemoryException exceção. Isso pode acontecer se o JSON contiver um token com cerca de 1 GB ou mais de tamanho, porque dobrar o tamanho de 1 GB resulta em um tamanho muito grande para caber em um int32 buffer.

ref struct limitações

Porque o Utf8JsonReader tipo é um ref struct, ele tem certas limitações. Por exemplo, ele não pode ser armazenado como um campo em uma classe ou estrutura diferente de um ref struct.

Para obter alto desempenho, Utf8JsonReader deve ser um ref struct, porque ele precisa armazenar em cache o byte> de entrada ReadOnlySpan<(que em si é um ref struct). Além disso, o Utf8JsonReader tipo é mutável, uma vez que detém estado. Portanto, passe-o por referência e não por valor. Passar o Utf8JsonReader valor by resultaria em uma cópia struct e as alterações de estado não seriam visíveis para o chamador.

Para obter mais informações sobre como usar ref structs, consulte Evitar alocações.

Ler texto UTF-8

Para obter o melhor desempenho possível ao usar Utf8JsonReadero , leia as cargas úteis JSON já codificadas como texto UTF-8 em vez de cadeias de caracteres UTF-16. Para obter um exemplo de código, consulte Filtrar dados usando Utf8JsonReader.

Ler com ReadOnlySequence de vários segmentos

Se sua entrada JSON for um byte> ReadOnlySpan<, cada elemento JSON poderá ser acessado a ValueSpan partir da propriedade no leitor à medida que você percorre o loop de leitura. No entanto, se sua entrada for um byte> ReadOnlySequence<(que é o resultado da leitura de um PipeReader), alguns elementos JSON podem atravessar vários segmentos do ReadOnlySequence<byte> objeto. Esses elementos não seriam acessíveis a partir de um bloco de ValueSpan memória contíguo. Em vez disso, sempre que você tiver um multisegmento ReadOnlySequence<byte> como entrada, sonde a HasValueSequence propriedade no leitor para descobrir como acessar o elemento JSON atual. Aqui está um padrão recomendado:

while (reader.Read())
{
    switch (reader.TokenType)
    {
        // ...
        ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
            reader.ValueSequence.ToArray() :
            reader.ValueSpan;
        // ...
    }
}

Ler vários documentos JSON

No .NET 9 e versões posteriores, você pode ler vários documentos JSON separados por espaço em branco de um único buffer ou fluxo. Por padrão, Utf8JsonReader lança uma exceção se detetar caracteres sem espaço em branco que rastreiam o primeiro documento de nível superior. No entanto, você pode configurar esse comportamento usando o JsonReaderOptions.AllowMultipleValues sinalizador.

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

Quando AllowMultipleValues estiver definido como true, você também poderá ler JSON de cargas úteis que contenham dados à direita que são JSON inválidos.

JsonReaderOptions options = new() { AllowMultipleValues = true };
Utf8JsonReader reader = new("[1,2,3]    <NotJson/>"u8, options);

reader.Read();
reader.Skip(); // Succeeds.
reader.Read(); // Throws JsonReaderException.

Para transmitir vários valores de nível superior, use o DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) ou DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken) sobrecarga. Por padrão, DeserializeAsyncEnumerable tenta transmitir elementos contidos em uma única matriz JSON de nível superior. Passe true o parâmetro para topLevelValues transmitir vários valores de nível superior.

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
 */

Pesquisas de nome de propriedade

Para procurar nomes de propriedades, não use ValueSpan para fazer comparações byte-a-byte chamando SequenceEqual. Em vez disso, chame ValueTextEquals, porque esse método não escapa de quaisquer caracteres que são escapados no JSON. Veja um exemplo que mostra como pesquisar um estabelecimento chamado "nome":

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;
    }
}

Ler valores nulos em tipos de valor anuláveis

As APIs internas System.Text.Json retornam apenas tipos de valor não anuláveis. Por exemplo, Utf8JsonReader.GetBoolean retorna um boolarquivo . Ele lança uma exceção se encontrar Null no JSON. Os exemplos a seguir mostram duas maneiras de manipular nulos, uma retornando um tipo de valor anulável e outra retornando o valor padrão:

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();
}

Pular filhos do token

Use o Utf8JsonReader.Skip() método para ignorar os filhos do token JSON atual. Se o tipo de token for JsonTokenType.PropertyName, o leitor se moverá para o valor da propriedade. O trecho de código a seguir mostra um exemplo de uso Utf8JsonReader.Skip() para mover o leitor para o valor de uma propriedade.

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;
    }
}

Consumir cadeias de caracteres JSON decodificadas

A partir do .NET 7, você pode usar o Utf8JsonReader.CopyString método em vez de consumir uma cadeia de Utf8JsonReader.GetString() caracteres JSON decodificada. Ao contrário GetString()do , que sempre aloca uma nova cadeia de caracteres, CopyString permite copiar a cadeia de caracteres sem escape para um buffer de sua propriedade. O trecho de código a seguir mostra um exemplo de consumo de uma cadeia de caracteres UTF-16 usando 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)
{
    // ...
}

Consulte também