Compartir a través de


Cómo usar Utf8JsonWriter en System.Text.Json

En este artículo se muestra cómo usar el tipo Utf8JsonReader para crear analizadores y deserializadores personalizados.

Utf8JsonReader es un lector de solo avance, de baja asignación y de alto rendimiento para texto JSON con codificación UTF-8 que se lee desde ReadOnlySpan<byte> o ReadOnlySequence<byte>. Utf8JsonReader es un tipo de bajo nivel que se puede usar para compilar analizadores y deserializadores personalizados. Los métodos JsonSerializer.Deserialize usan Utf8JsonReader en segundo plano.

Utf8JsonReader no se puede usar directamente desde el código de Visual Basic. Para obtener más información, vea Compatibilidad con Visual Basic.

En el siguiente ejemplo, se muestra cómo utilizar la clase 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

En el código anterior se supone que la variable jsonUtf8 es una matriz de bytes que contiene JSON válido, con codificación UTF-8.

Filtrado de datos mediante Utf8JsonReader

En el ejemplo siguiente se muestra cómo leer un archivo de forma sincrónica y buscar un 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

Puede encontrar una versión asincrónica de este ejemplo en Proyecto JSON de ejemplos de .NET.

El código anterior:

  • Se supone que el código JSON contiene una matriz de objetos y que cada objeto puede contener una propiedad "name" de tipo cadena.

  • Cuenta los objetos y los valores de propiedad "name" que terminan en "University".

  • Supone que el archivo tiene codificación UTF-16 y lo transcodifica a UTF-8. Un archivo con codificación UTF-8 puede leerse directamente en ReadOnlySpan<byte> mediante el código siguiente:

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

    Si el archivo contiene una marca BOM UTF-8, quítela antes de pasar los bytes a Utf8JsonReader, ya que el lector espera texto. De lo contrario, la marca BOM se considera JSON no válido y el lector inicia una excepción.

Aquí se muestra un ejemplo de JSON que el código anterior puede leer. El mensaje de resumen resultante es "2 de 4 tienen nombres que terminan en '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"
  }
]

Lectura de una secuencia mediante Utf8JsonReader

Al leer un archivo grande (un gigabyte o más de tamaño, por ejemplo), puede que desee evitar tener que cargar todo el archivo en la memoria de una vez. En este escenario, puede usar FileStream.

Al usar Utf8JsonReader para leer de una secuencia, se aplican las siguientes reglas:

  • El búfer que contiene la carga parcial JSON debe ser al menos tan grande como el token JSON más grande que contiene para que el lector pueda avanzar.
  • El búfer debe ser al menos tan grande como la secuencia más grande de espacio en blanco dentro del JSON.
  • El lector no realiza un seguimiento de los datos que ha leído hasta que lea completamente el TokenType siguiente en la carga JSON. Por tanto, cuando haya bytes restantes en el búfer, tendrá que volver a pasarlos al lector. Puede usar BytesConsumed para determinar el número de bytes que quedan.

El código siguiente muestra cómo leer desde una secuencia. Este ejemplo se muestra MemoryStream. Un código similar funcionará con FileStream, excepto cuando FileStream contenga una marca BOM UTF-8 al principio. En ese caso, debe quitar esos tres bytes del búfer antes de pasar los bytes restantes a Utf8JsonReader. En caso contrario, el lector produciría una excepción, ya que la marca BOM no se considera una parte válida del JSON.

El código de ejemplo comienza con un búfer de 4 KB y duplica su tamaño cada vez que encuentra que no es lo suficientemente grande como para ajustarse a un token JSON completo, lo que es necesario para que el lector realice el progreso de la carga de JSON. El ejemplo de JSON proporcionado en el fragmento de código desencadena un aumento del tamaño del búfer solo si se establece un tamaño de búfer inicial muy pequeño, por ejemplo, 10 bytes. Si establece el tamaño de búfer inicial en 10, las instrucciones Console.WriteLine muestran la causa y el efecto de los aumentos del tamaño del búfer. Con un tamaño de búfer inicial de 4 KB, cada Console.WriteLine muestra todo el código JSON de ejemplo y nunca es necesario aumentar el tamaño del búfer.

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

En el ejemplo anterior no se establece ningún límite para el tamaño del búfer. Si el tamaño del token es demasiado grande, se podría producir un error en el código con una excepción OutOfMemoryException. Esto puede ocurrir si el archivo JSON contiene un token de aproximadamente 1 GB o más de tamaño, ya que la duplicación del tamaño de 1 GB da como resultado un tamaño demasiado grande para caber en un búfer de int32.

Limitaciones de la estructura de referencia

Dado que el tipo Utf8JsonReader es una estructura de referencia, tiene ciertas limitaciones. Por ejemplo, no se puede almacenar como un campo en una clase o estructura que no sea una estructura de referencia.

Para lograr un alto rendimiento, este tipo debe ser ref struct, ya que necesita almacenar en caché la entrada ReadOnlySpan<byte>, que en sí misma es una estructura de referencia. Además, el tipo Utf8JsonReader es mutable, ya que contiene el estado. Por tanto, páselo por referencia en lugar de por valor. Si se pasa por valor, se producirá una copia de la estructura y los cambios de estado no serán visibles para el autor de la llamada.

Para más información sobre el uso de las estructuras de referencia, vea Evitar asignaciones.

Lectura de texto UTF-8

Para lograr el mejor rendimiento posible mientras usa Utf8JsonReader, lea las cargas JSON ya codificadas como texto UTF-8 en lugar de como cadenas UTF-16. Para obtener un ejemplo de código, vea Filtrado de datos mediante Utf8JsonReader.

Lectura con ReadOnlySequence de varios segmentos

Si la entrada JSON es ReadOnlySpan<byte>, se puede acceder a cada elemento JSON desde la propiedad ValueSpan en el lector a medida que se avanza por el bucle de lectura. Pero si la entrada es ReadOnlySequence<byte> (que es el resultado de la lectura de PipeReader), algunos elementos JSON podrían ocupar varios segmentos del objeto ReadOnlySequence<byte>. No se puede acceder a estos elementos desde ValueSpan en un bloque de memoria contiguo. En su lugar, siempre que tenga un ReadOnlySequence<byte> de varios segmentos como entrada, sondee la propiedad HasValueSequence en el lector para averiguar cómo acceder al elemento JSON actual. Este es un patrón recomendado:

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

Uso de ValueTextEquals para las búsquedas de nombres de propiedad

No use ValueSpan para realizar comparaciones byte a byte mediante una llamada a SequenceEqual para las búsquedas de nombres de propiedad. En su lugar, llame a ValueTextEquals, ya que ese método anula el escape de caracteres que se van a escapar en JSON. Este es un ejemplo en el que se muestra cómo buscar una propiedad denominada "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;
    }
}

Lectura de valores NULL en tipos de valor que aceptan valores NULL

Las API integradas de System.Text.Json solo devuelven tipos de valor que no aceptan valores NULL. Por ejemplo, Utf8JsonReader.GetBoolean devuelve bool. Si encuentra Null en el elemento JSON, inicia una excepción. En los siguientes ejemplos se muestran dos formas de controlar valores NULL: una devolviendo un tipo de valor que acepta valores NULL y otra devolviendo el valor predeterminado:

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

Omisión de elementos secundarios del token

Use el método Utf8JsonReader.Skip() para omitir los elementos secundarios del token JSON actual. Si el tipo de token es JsonTokenType.PropertyName, el lector se mueve al valor de propiedad. En el fragmento de código siguiente se muestra un ejemplo de uso de Utf8JsonReader.Skip() para mover el lector al valor de una propiedad .

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

Consumo de cadenas JSON descodificadas

A partir de .NET 7, puede usar el método Utf8JsonReader.CopyString en lugar de Utf8JsonReader.GetString() para consumir una cadena JSON descodificada. A diferencia de GetString(), que siempre asigna una nueva cadena, CopyString le permite copiar la cadena sin escape en un búfer de su propiedad. En el fragmento de código siguiente se muestra un ejemplo de consumo de una cadena UTF-16 mediante 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 también