Como usar Utf8JsonReader no System.Text.Json

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

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

O Utf8JsonReader não pode ser usado diretamente a partir do código do Visual Basic. Para saber mais, confira Suporte para Visual Basic.

O exemplo a seguir mostra como usar a classe 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

O código anterior pressupõe que a variável jsonUtf8 seja uma matriz de bytes que contém JSON válido, codificado como UTF-8.

Filtrar dados usando Utf8JsonReader

O exemplo a seguir mostra como ler um arquivo de forma síncrona e pesquisar 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://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

Para obter uma versão assíncrona deste exemplo, consulte o projeto JSON de exemplos do .NET.

O código anterior:

  • Pressupõe que o JSON contém uma matriz de objetos e cada objeto pode conter uma propriedade "name" da cadeia de caracteres de tipo.

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

  • Pressupõe que o arquivo seja codificado como UTF-16 e transcodifica-o 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 UTF-8 (BOM), remova-a antes de passar os bytes para o Utf8JsonReader, já que o leitor espera texto. Caso contrário, o BOM é considerado JSON inválido e o leitor gera uma exceção.

Aqui está um exemplo JSON que o código anterior pode ler. A mensagem de resumo resultante é "2 em 4 têm nomes que terminam com '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"
  }
]

Ler de um fluxo usando Utf8JsonReader

Ao ler um arquivo grande (um gigabyte ou mais em tamanho, por exemplo), convém evitar a necessidade de carregar todo o arquivo na memória de uma vez. Para este cenário, é possível usar FileStream.

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

  • O buffer que contém um conteúdo JSON parcial deve ser pelo menos tão grande quanto o maior token JSON dentro dele para que o leitor possa avançar.
  • 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 no conteúdo JSON. Portanto, quando há bytes restantes no buffer, você precisa passá-los para o leitor novamente. Você pode usar BytesConsumed para determinar quantos bytes sobraram.

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

O código de exemplo começa com um buffer de 4KB e dobra o tamanho do buffer sempre que ele descobre que o tamanho não é grande o suficiente para se ajustar a um token JSON completo, o que é necessário para que o leitor faça progresso no conteúdo JSON. O exemplo JSON fornecido no snippet dispara um aumento de tamanho de 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 instruções Console.WriteLine ilustram a causa e o efeito dos aumentos de tamanho do buffer. No tamanho inicial do buffer de 4KB, todo o JSON de exemplo é mostrado por cada 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://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

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

Limitações da estrutura ref

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

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

Para obter mais informações sobre como usar structs de referência, confira Evitar alocações.

Ler texto UTF-8

Para obter o melhor desempenho possível ao usar Utf8JsonReader, o conteúdo JSON de gravação já codificado como texto UTF-8 em vez de como 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 ReadOnlySpan<byte>, cada elemento JSON poderá ser acessado a partir da propriedade ValueSpan no leitor conforme você percorre o loop de leitura. No entanto, se sua entrada for um ReadOnlySequence<byte> (que é o resultado da leitura de PipeReader), alguns elementos JSON poderão abranger vários segmentos do ReadOnlySequence<byte> objeto. Esses elementos não seriam acessíveis de ValueSpan em um bloco de memória contíguo. Em vez disso, sempre que você tiver um multissegmento ReadOnlySequence<byte> como entrada, sondar a propriedade HasValueSequence 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;
        // ...
    }
}

Usar ValueTextEquals para pesquisas de nome de propriedade

Não use ValueSpan para fazer comparações byte por byte chamando SequenceEqual para pesquisas de nome de propriedade. Em vez disso, chame ValueTextEquals porque esse método desescala todos os caracteres que são escapados no JSON. Aqui está um exemplo que mostra como pesquisar uma propriedade chamada "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;
    }
}

Ler valores nulos em tipos de valor anulável

As APIs internas de System.Text.Json retornam apenas tipos de valor não anuláveis. Por exemplo, Utf8JsonReader.GetBoolean retorna bool. Ele gerará uma exceção se encontrar Null no JSON. Os exemplos a seguir mostram duas maneiras de lidar com 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();
}

Ignorar filhos do token

Use o método Utf8JsonReader.Skip() para ignorar os filhos do token JSON atual. Se o tipo de token for JsonTokenType.PropertyName, o leitor passará para o valor da propriedade. O snippet de código a seguir mostra um exemplo de como usar 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 método Utf8JsonReader.CopyString, em vez de Utf8JsonReader.GetString() para consumir uma cadeia de caracteres JSON decodificada. Ao contrário de GetString(), que sempre aloca uma nova cadeia de caracteres, CopyString permite copiar a cadeia de caracteres sem escape para um buffer que você possui. O snippet 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)
{
    // ...
}

Confira também