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 alto rendimiento y baja asignación para texto JSON codificado en UTF-8. El texto se lee desde un archivo ReadOnlySpan<byte> o ReadOnlySequence<byte>. Utf8JsonReader es un tipo de bajo nivel que puede utilizarse para construir analizadores sintácticos y deserializadores personalizados. (Los métodos JsonSerializer.Deserialize usan Utf8JsonReader en segundo plano).

En el siguiente ejemplo, se muestra cómo utilizar la clase Utf8JsonReader. Este código asume que la variable jsonUtf8Bytes es una matriz de bytes que contiene 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://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

Nota:

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

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

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

Sugerencia

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

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. En el tamaño de búfer inicial de 4 KB, se muestra todo el JSON de ejemplo en cada llamada a Console.WriteLine y el tamaño del búfer nunca se debe aumentar.

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

Debido a que el tipo Utf8JsonReader es un ref struct, tiene ciertas limitaciones. Por ejemplo, no puede almacenarse como campo en una clase o estructura que no sea un ref struct.

Para conseguir un alto rendimiento, Utf8JsonReader debe ser un ref struct, porque necesita almacenar en caché el ReadOnlySpan<byte> de entrada (que a su vez es un ref struct). Además, el tipo Utf8JsonReader es mutable, ya que contiene el estado. Por tanto, páselo por referencia en lugar de por valor. Pasar el valor by Utf8JsonReader resultaría en una copia de struct, y los cambios de estado no serían visibles para el invocador.

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

Leer varios documentos JSON

En .NET 9 y versiones posteriores, puedes leer múltiples documentos JSON separados por espacios en blanco desde un único buffer o stream. Por defecto, Utf8JsonReader lanza una excepción si detecta cualquier carácter que no sea un espacio en blanco que siga al primer documento de nivel superior. Sin embargo, puede configurar este comportamiento usando la marca 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

Cuando AllowMultipleValues se establece en true, también puede leer JSON de cargas útiles que contengan datos finales que no sean JSON vá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 varios valores de nivel superior, use la sobrecarga DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) o DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken). Por defecto, DeserializeAsyncEnumerable intenta transmitir elementos contenidos en una única matriz JSON de nivel superior. Pase true por el parámetro topLevelValues para transmitir varios valores de nivel 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
 */

Búsqueda de nombres de propiedades

Para buscar nombres de propiedades, no use ValueSpan para realizar comparaciones byte a byte llamando a SequenceEqual. En su lugar, llame a ValueTextEquals, porque este método desescapa cualquier carácter que esté escapado en el 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