Partager via


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 hautes performances et à faible allocation de type forward-only pour le texte JSON codé au format UTF-8 et lu à partir de ReadOnlySpan<byte> ou ReadOnlySequence<byte>. Utf8JsonReader est un type de bas niveau, permettant de générer des analyseurs et des désérialiseurs personnalisés. Les méthodes JsonSerializer.Deserialize utilisent Utf8JsonReader sous les couvertures.

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

L’exemple suivant illustre la 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

Le code précédent suppose que la variable jsonUtf8 est un tableau d’octets qui contient un JSON valide, encodé en UTF-8.

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

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

Le code précédent :

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

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 pouvez éviter d’avoir à charger l’ensemble du fichier en mémoire à la fois. 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. À la taille de mémoire tampon initiale de 4 Ko, l’ensemble de l’exemple JSON est affiché par chaque Console.WriteLine, et la taille de la mémoire tampon n’a jamais à ê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://learn.microsoft.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 une struct de référence, il a certaines limitations. Par exemple, il ne peut pas être stocké en tant que champ sur une classe ou un struct autre qu’un struct de référence.

Pour obtenir des performances élevées, ce type doit être un ref struct, car il doit mettre en cache le ReadOnlySpan<byte> d’entrée, qui est lui-même un struct de référence. 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. Le passage par valeur entraînerait une copie du struct, 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;
        // ...
    }
}

Utiliser ValueTextEquals pour les recherches de noms de propriété

N’utilisez pas ValueSpan pour effectuer des comparaisons octet par octet en appelant SequenceEqual pour les recherches de noms de propriété. Appelez ValueTextEquals à la place, car cette méthode annule l’échappement de tous les caractères qui sont placés dans une séquence d’échappement 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