Comment utiliser Utf8JsonReader dans System.Text.Json

Cet article explique comment vous pouvez utiliser le type Utf8JsonReader pour créer des analyseurs et des désérialiseurs personnalisés.

Utf8JsonReader est un lecteur haute performance, à faible allocation, orienté avant uniquement, pour du texte JSON encodé en UTF-8. Le texte est lu à partir d'un ReadOnlySpan<byte> ou d'un ReadOnlySequence<byte>. Utf8JsonReader est un type de bas niveau qui peut être utilisé pour créer des analyseurs et des désérialiseurs personnalisés. (Les méthodes JsonSerializer.Deserialize utilisent Utf8JsonReader en interne).

L'exemple suivant illustre la classe Utf8JsonReader. Ce code suppose que la variable jsonUtf8Bytes est un tableau d’octets contenant du JSON valide, encodé en 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

Remarque

Utf8JsonReader ne peut pas être utilisé directement à partir de code Visual Basic. Pour plus d’informations, consultez le Support Visual Basic.

Filtrer des données à l’aide de Utf8JsonReader

L’exemple suivant montre comment lire de manière synchrone un fichier et rechercher une valeur.

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

Le code précédent :

  • Il suppose que le JSON contient un tableau d'objets, et que chaque objet peut contenir une propriété « name » de type chaîne.

  • Compte les objets et les valeurs de propriété « name » qui se terminent par « University ».

  • Suppose que le fichier est encodé en UTF-16 et le transcode en UTF-8.

    Un fichier encodé en UTF-8 peut être lu directement dans un ReadOnlySpan<byte> à l’aide du code suivant :

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

    Si le fichier contient une marque d’ordre d’octet UTF-8, supprimez-la avant de passer les octets au Utf8JsonReader, car le lecteur attend du texte. Sinon, la nomenclature est considérée comme du JSON non valide et le lecteur lève une exception.

Voici un exemple de JSON que le code précédent peut lire. Le message de résumé résultant est « 2 sur 4 ont des noms qui se terminent par ’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"
  }
]

Conseil

Pour obtenir une version asynchrone de cet exemple, consultez Projet JSON d’exemples .NET.

Lire à partir d’un flux à l’aide de Utf8JsonReader

Lors de la lecture d'un fichier volumineux (d'une taille d'un gigaoctet ou plus, par exemple), vous pourriez vouloir éviter de charger l'intégralité du fichier en mémoire d'un seul coup. Pour ce scénario, vous pouvez utiliser un FileStream.

Lorsque vous utilisez le Utf8JsonReader pour lire à partir d’un flux, les règles suivantes s’appliquent :

  • La mémoire tampon contenant la charge utile JSON partielle doit être au moins aussi grande que le plus grand jeton JSON qu’elle contient afin que le lecteur puisse progresser.
  • La mémoire tampon doit être au moins aussi grande que la plus grande séquence d’espaces blancs dans le JSON.
  • Le lecteur ne suit pas les données qu’il a lues tant qu’il n’a pas lu entièrement le TokenType suivant dans la charge utile JSON. Par conséquent, lorsqu’il reste des octets dans la mémoire tampon, vous devez les transmettre à nouveau au lecteur. Vous pouvez utiliser BytesConsumed pour déterminer le nombre d’octets restants.

Le code suivant montre comment lire à partir d’un flux. L’exemple montre un MemoryStream. Tout code similaire fonctionnera avec un FileStream, sauf lorsque contient FileStream une nomenclature UTF-8 au début. Dans ce cas, vous devez supprimer ces trois octets de la mémoire tampon avant de passer les octets restants au Utf8JsonReader. Sinon, le lecteur lève une exception, car la nomenclature n’est pas considérée comme une partie valide du JSON.

L’exemple de code commence par une mémoire tampon de 4 Ko et double la taille de la mémoire tampon chaque fois qu’il constate que la taille n’est pas assez grande pour un jeton JSON complet, ce qui est nécessaire pour que le lecteur progresse sur la charge utile JSON. L’exemple JSON fourni dans l’extrait de code déclenche une augmentation de la taille de la mémoire tampon uniquement si vous définissez une taille de mémoire tampon initiale très petite, par exemple 10 octets. Si vous définissez la taille de la mémoire tampon initiale sur 10, les instructions Console.WriteLine illustrent la cause et l’effet de l’augmentation de la taille de la mémoire tampon. Avec une taille de tampon initiale de 4 Ko, tout l'exemple de JSON est affiché à chaque appel de Console.WriteLine, et la taille du tampon n'a jamais besoin d'être augmentée.

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

L’exemple précédent ne définit aucune limite à la taille de la mémoire tampon. Si la taille du jeton est trop grande, le code peut échouer avec une exception OutOfMemoryException. Cela peut se produire si le JSON contient un jeton d’environ 1 Go ou plus, car le doublement de la taille de 1 Go entraîne une taille trop grande pour tenir dans une mémoire tampon int32.

Limitations ref struct

Étant donné que le type Utf8JsonReader est un ref struct, il présente certaines limitations. Par exemple, il ne peut pas être stocké en tant que champ dans une classe ou une structure autre qu'un ref struct.

Pour atteindre des performances élevées, Utf8JsonReader doit être un ref struct, car il a besoin de mettre en cache l'entrée ReadOnlySpan<byte> (qui est elle-même un ref struct). En outre, le type Utf8JsonReader est mutable, car il contient l’état. Par conséquent, transmettez-le par référence plutôt que par valeur. Passer le Utf8JsonReader par valeur entraînerait une copie de la structure, et les changements d'état ne seraient pas visibles par l'appelant.

Pour plus d’informations sur l’utilisation de structs ref, consultez Éviter les allocations.

Lire du texte UTF-8

Pour obtenir les meilleures performances possibles lors de l’utilisation de Utf8JsonReader, lisez des charges utiles JSON déjà encodées en tant que texte UTF-8 plutôt que sous forme de chaînes UTF-16. Pour obtenir un exemple de code, consultez Filtrer les données à l’aide d’Utf8JsonReader.

Lecture avec ReadOnlySequence multi-segment

Si votre entrée JSON est un ReadOnlySpan<byte>, chaque élément JSON est accessible à partir de la propriété ValueSpan sur le lecteur lorsque vous passez par la boucle de lecture. Toutefois, si votre entrée est un ReadOnlySequence<byte> (qui est le résultat de la lecture à partir d’un PipeReader), certains éléments JSON peuvent chevaucher plusieurs segments de l’objet ReadOnlySequence<byte>. Ces éléments ne sont pas accessibles à partir de ValueSpan dans un bloc de mémoire contigu. Au lieu de cela, chaque fois que vous avez un ReadOnlySequence<byte> multi-segment comme entrée, interrogez la propriété HasValueSequence sur le lecteur pour déterminer comment accéder à l’élément JSON actuel. Voici un modèle recommandé :

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

Lire plusieurs documents JSON

Dans .NET 9 et les versions ultérieures, vous pouvez lire plusieurs documents JSON séparés par des espaces blancs à partir d'un seul tampon ou flux. Par défaut, Utf8JsonReader lève une exception s'il détecte des caractères non-blancs après le premier document de niveau supérieur. Cependant, vous pouvez configurer ce comportement à l'aide du drapeau 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

Lorsque AllowMultipleValues est défini sur true, vous pouvez également lire du JSON à partir de charges utiles contenant des données finales invalides pour du JSON.

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

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

Pour diffuser plusieurs valeurs de niveau supérieur, utilisez la surcharge DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) ou DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken). Par défaut, DeserializeAsyncEnumerable tente de diffuser les éléments contenus dans un tableau JSON de niveau supérieur unique. Passez true pour le paramètre topLevelValues afin de diffuser plusieurs valeurs de niveau supérieur.

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

Recherches de noms de propriétés

Pour rechercher des noms de propriétés, n'utilisez pas ValueSpan pour effectuer des comparaisons octet par octet en appelant SequenceEqual. Utilisez plutôt ValueTextEquals, car cette méthode annule l'échappement des caractères qui sont échappés dans le JSON. Voici un exemple qui montre comment rechercher une propriété nommée « 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;
    }
}

Lire des valeurs null dans des types de valeurs pouvant être null

Les API intégrées System.Text.Json retournent uniquement des types de valeurs non nullables. Par exemple, Utf8JsonReader.GetBoolean retourne un bool. Le code lève une exception si Null est présent dans le JSON. Les exemples suivants illustrent deux façons de gérer les valeurs null, l’une en retournant un type de valeur nullable et l’autre en retournant la valeur par défaut :

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

Ignorer les enfants du jeton

Utilisez la méthode Utf8JsonReader.Skip() pour ignorer les enfants du jeton JSON actuel. Si le type de jeton est JsonTokenType.PropertyName, le lecteur passe à la valeur de propriété. L’extrait de code suivant montre un exemple d’utilisation de Utf8JsonReader.Skip() pour déplacer le lecteur vers la valeur d’une propriété.

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

Consommer des chaînes JSON décodées

À partir de .NET 7, vous pouvez utiliser la méthode Utf8JsonReader.CopyString au lieu de Utf8JsonReader.GetString() pour consommer une chaîne JSON décodée. Contrairement à GetString(), qui alloue toujours une nouvelle chaîne, CopyString vous permet de copier la chaîne non échappée dans une mémoire tampon que vous possédez. L’extrait de code suivant montre un exemple de consommation d’une chaîne UTF-16 à l’aide de 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)
{
    // ...
}

Voir aussi