Condividi tramite


Come usare Utf8JsonReader in System.Text.Json

Questo articolo illustra come usare il tipo Utf8JsonReader per la creazione di parser e deserializzatori personalizzati.

Utf8JsonReader è un lettore forward-only con prestazioni elevate e a prestazioni elevate per il testo JSON con codifica UTF-8. Il testo viene letto da un ReadOnlySpan<byte> oggetto o ReadOnlySequence<byte>. Utf8JsonReader è un tipo di basso livello che può essere usato per creare parser e deserializzatori personalizzati. (I JsonSerializer.Deserialize metodi usano Utf8JsonReader sotto le quinte.

Nell'esempio seguente viene illustrato come utilizzare la classe Utf8JsonReader. Questo codice presuppone che la jsonUtf8Bytes variabile sia una matrice di byte che contiene codice JSON valido, codificato come 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 non può essere usato direttamente dal codice Visual Basic. Per ottenere ulteriori informazioni, consultare l'articolo Supporto di Visual Basic.

Filtrare i dati usando Utf8JsonReader

Nel seguente esempio viene illustrato come leggere un file in modo sincrono e cercare un valore.

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

Il codice precedente:

  • Si presuppone che il codice JSON contenga una matrice di oggetti e ogni oggetto possa contenere una proprietà "name" di tipo string.

  • Conta gli oggetti e i valori delle proprietà "nome" che terminano con "University".

  • Presuppone che il file sia codificato come UTF-16 e lo transcodifica in UTF-8.

    Un file codificato come UTF-8 può essere letto direttamente in un ReadOnlySpan<byte> usando il seguente codice:

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

    Se il file contiene un byte order mark UTF-8 (BOM), rimuoverlo prima di passare i byte al Utf8JsonReader, poiché il lettore si aspetta del testo. In caso contrario, il BOM viene considerato JSON non valido e il lettore genera un'eccezione.

Ecco un esempio JSON che il codice precedente può leggere. Il messaggio di riepilogo risultante è "2 su 4 hanno nomi che terminano con '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"
  }
]

Suggerimento

Per una versione asincrona di questo esempio, vedere Progetto JSON di esempi .NET.

Leggere da un flusso usando Utf8JsonReader

Quando si legge un file di grandi dimensioni (ad esempio, un gigabyte o più dimensioni), è possibile evitare di caricare l'intero file in memoria contemporaneamente. Per questo scenario, è possibile usare un FileStream.

Quando si usa Utf8JsonReader per leggere da un flusso, si applicano le seguenti regole:

  • Il buffer contenente il payload JSON parziale deve essere grande almeno quanto il token JSON più grande al suo interno, in modo che il lettore possa avanzare.
  • Il buffer deve essere grande almeno quanto la più grande sequenza di spazi vuoti all'interno del JSON.
  • Il lettore non tiene traccia dei dati letti finché non legge completamente il TokenType successivo nel payload JSON. Quindi, quando sono presenti byte rimanenti nel buffer, è necessario passarli di nuovo al lettore. È possibile usare BytesConsumed per determinare il numero di byte rimanenti.

Il codice seguente illustra come leggere da un flusso. Nell'esempio viene illustrato un MemoryStream. Un codice simile funzionerà con un FileStream, tranne quando FileStream contiene un BOM UTF-8 all'inizio. In tal caso, è necessario rimuovere questi tre byte dal buffer prima di passare i byte rimanenti al Utf8JsonReader. In caso contrario, il lettore genera un'eccezione, poiché il BOM non è considerato una parte valida del JSON.

Il codice di esempio inizia con un buffer di 4 kB e raddoppia la dimensione del buffer ogni volta che scopre che la dimensione non è sufficiente per inserire un token JSON completo, necessario al lettore per avanzare nel payload JSON. L'esempio di JSON fornito nel frammento attiva un aumento delle dimensioni del buffer solo se si impostano dimensioni del buffer iniziali molto ridotte, ad esempio 10 byte. Se si imposta la dimensione iniziale del buffer su 10, le istruzioni Console.WriteLine illustrano la causa e l'effetto dell'aumento delle dimensioni del buffer. Con le dimensioni iniziali del buffer di 4 KB, l'intero codice JSON di esempio viene visualizzato da ogni chiamata a Console.WriteLinee le dimensioni del buffer non devono mai essere aumentate.

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

Nell'esempio precedente non viene impostato alcun limite alla dimensione del buffer. Se le dimensioni del token sono troppo grandi, il codice potrebbe non riuscire con un'eccezione OutOfMemoryException. Ciò può accadere se il JSON contiene un token di dimensioni pari o superiori a 1 GB, perché raddoppiando la dimensione di 1 GB si ottiene una dimensione troppo grande per essere inserita in un buffer int32.

limitazioni dello struct di riferimento

Poiché il Utf8JsonReader tipo è , ref structpresenta alcune limitazioni. Ad esempio, non può essere archiviato come campo in una classe o uno struct diverso da .ref struct

Per ottenere prestazioni elevate, Utf8JsonReader deve essere un ref struct, perché deve memorizzare nella cache il byte> ReadOnlySpan<di input (che si tratta di un ).ref struct Inoltre, il tipo di Utf8JsonReader è modificabile poiché contiene uno stato. Di conseguenza, è necessario passarlo per riferimento anziché per valore. Il passaggio di per Utf8JsonReader valore comporta una copia dello struct e le modifiche dello stato non saranno visibili al chiamante.

Per ottenere ulteriori informazioni su come usare gli struct di riferimento, consultare l'articolo Ridurre le allocazioni di memoria usando nuove funzionalità C#.

Leggere testo UTF-8

Per ottenere prestazioni ottimali durante l'uso di Utf8JsonReader, leggere i payload JSON già codificati come testo UTF-8 anziché come stringhe UTF-16. Per un esempio di codice, vedere Filtrare i dati usando Utf8JsonReader.

Lettura con multi segmento ReadOnlySequence

Se l'input JSON è un byte ReadOnlySpan<>, è possibile accedere a ogni elemento JSON dalla proprietà ValueSpan nel lettore durante il ciclo di lettura. Tuttavia, se l'input è un byte ReadOnlySequence<>(che è il risultato della lettura da un PipeReader), alcuni elementi JSON potrebbero avere più segmenti dell'oggetto ReadOnlySequence<byte>. Questi elementi non sarebbero accessibili da ValueSpan in un blocco di memoria contiguo. Al contrario, ogni volta che si ha un multi segmento ReadOnlySequence<byte> come input, è necessario eseguire il polling della proprietà HasValueSequence sul lettore per capire come accedere all'elemento JSON corrente. Si riporta di seguito un modello consigliato:

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

Leggere più documenti JSON

In .NET 9 e versioni successive è possibile leggere più documenti JSON separati da spazi vuoti da un singolo buffer o flusso. Per impostazione predefinita, Utf8JsonReader genera un'eccezione se rileva eventuali caratteri non vuoti che contano il primo documento di primo livello. Tuttavia, è possibile configurare tale comportamento usando il JsonReaderOptions.AllowMultipleValues flag .

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

Quando AllowMultipleValues è impostato su true, è anche possibile leggere JSON dai payload che contengono dati finali non validi per JSON.

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

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

Per trasmettere più valori di primo livello, usare l'overload DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) o DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken) . Per impostazione predefinita, DeserializeAsyncEnumerable tenta di trasmettere elementi contenuti in una singola matrice JSON di primo livello. Passare true per il topLevelValues parametro per trasmettere più valori di primo livello.

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

Ricerche dei nomi delle proprietà

Per cercare i nomi delle proprietà, non usare ValueSpan per eseguire confronti byte per byte chiamando SequenceEqual. Chiamare ValueTextEqualsinvece , perché questo metodo annulla l'escape di tutti i caratteri di escape nel codice JSON. Ecco un esempio che mostra come cercare una proprietà denominata "nome":

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

Leggere i valori Null in tipi di valore che ammettono i valori Null

Le API predefinite System.Text.Json restituiscono solo tipi valore che non ammettono i valori Null. Ad esempio, Utf8JsonReader.GetBoolean restituisce un bool. Generare un'eccezione se si trova Null nel JSON. Gli esempi seguenti illustrano due modi per gestire i valori Null, uno restituendo un tipo di valore che ammette i valori Null e uno restituendo il valore predefinito:

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

Ignorare gli elementi figlio del token

Usare il metodo Utf8JsonReader.Skip() per ignorare gli elementi figlio del token JSON corrente. Se il tipo di token è JsonTokenType.PropertyName, il lettore passa al valore della proprietà. Il frammento di codice seguente mostra un esempio dell’utilizzo di Utf8JsonReader.Skip() per spostare il lettore nel valore di una proprietà.

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

Utilizzare stringhe JSON decodificate

A partire da .NET 7, è possibile usare il metodo Utf8JsonReader.CopyString anziché Utf8JsonReader.GetString() per usare una stringa JSON decodificata. A differenza di GetString(), che alloca sempre una nuova stringa, CopyString consente di copiare la stringa senza caratteri di escape in un buffer di cui si è proprietari. Il frammento di codice seguente mostra un esempio di utilizzo di una stringa 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)
{
    // ...
}

Vedi anche