Udostępnij za pośrednictwem


Jak używać Utf8JsonReader w System.Text.Json

W tym artykule pokazano, jak używać typu Utf8JsonReader w celu tworzenia niestandardowych analizatorów i deserializatorów.

Utf8JsonReader to wysokowydajny, niski zużycie zasobów, czytnik jednokierunkowy dla tekstu JSON zakodowanego w formacie UTF-8. Tekst jest odczytywany z elementu ReadOnlySpan<byte> lub ReadOnlySequence<byte>. Utf8JsonReader jest typem niskiego poziomu, który może być używany do tworzenia niestandardowych analizatorów i deserializatorów. (Metody JsonSerializer.Deserialize wykorzystują Utf8JsonReader w tle.)

W poniższym przykładzie pokazano, jak używać Utf8JsonReader klasy . Ten kod zakłada, że zmienna jsonUtf8Bytes jest tablicą bajtów zawierającą prawidłowy kod JSON zakodowany jako 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

Uwaga

Utf8JsonReader Nie można używać bezpośrednio z poziomu programu Visual Basic Code. Aby uzyskać więcej informacji, zobacz Obsługa języka Visual Basic.

Filtrowanie danych przy użyciu Utf8JsonReader

W poniższym przykładzie pokazano, jak synchronicznie odczytać plik i wyszukać wartość.

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

Poprzedni kod:

  • Zakłada się, że JSON zawiera tablicę obiektów, a każdy obiekt może zawierać właściwość "name" typu string.

  • Zlicza obiekty i wartości właściwości "name", które kończą się na "University".

  • Zakłada, że plik jest zakodowany jako UTF-16 i transkoduje go do formatu UTF-8.

    Plik zakodowany jako UTF-8 można odczytać bezpośrednio do obiektu ReadOnlySpan<byte> przy użyciu następującego kodu:

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

    Jeśli plik zawiera znacznik kolejności bajtów UTF-8 (BOM), usuń go przed przekazaniem bajtów do Utf8JsonReader, ponieważ czytelnik oczekuje tekstu. W przeciwnym razie znacznik BOM jest uznawany za nieprawidłowy JSON, a parser zgłasza wyjątek.

Oto przykładowy kod JSON, który można odczytać z poprzedniego kodu. Wynikowy komunikat podsumowujący to "2 na 4 nazwy kończą się słowem '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"
  }
]

Wskazówka

Aby zapoznać się z asynchroniczną wersją tego przykładu, sprawdź przykładowy projekt JSON dla platformy .NET.

Odczytywanie ze strumienia przy użyciu Utf8JsonReader

Podczas odczytywania dużego pliku (na przykład o rozmiarze gigabajta lub większego) możesz chcieć uniknąć ładowania całego pliku do pamięci na raz. W tym scenariuszu można użyć elementu FileStream.

W przypadku używania elementu Utf8JsonReader do odczytu ze strumienia obowiązują następujące reguły:

  • Bufor zawierający częściowy ładunek JSON musi być co najmniej tak duży, jak największy w nim token JSON, aby umożliwić czytelnikowi postęp.
  • Bufor musi być co najmniej tak duży, jak największa sekwencja białych znaków w formacie JSON.
  • Czytelnik nie śledzi danych, które przeczytał, dopóki nie zostanie całkowicie odczytany następny element TokenType w ładunku JSON. Więc gdy w buforze są pozostawione bajty, należy je ponownie przekazać do czytnika. Możesz użyć BytesConsumed polecenia , aby określić, ile bajtów pozostało.

Poniższy kod ilustruje sposób odczytywania ze strumienia. W przykładzie pokazano element MemoryStream. Podobny kod będzie działać z elementem FileStream, z wyjątkiem sytuacji, gdy element FileStream zawiera kod UTF-8 BOM na początku. W takim przypadku należy usunąć te trzy bajty z buforu przed przekazaniem pozostałych bajtów do .Utf8JsonReader W przeciwnym razie czytelnik zgłosi wyjątek, ponieważ model BOM nie jest uważany za prawidłową część kodu JSON.

Przykładowy kod rozpoczyna się od buforu o rozmiarze 4 KB i podwaja rozmiar buforu za każdym razem, gdy stwierdza, że rozmiar nie jest wystarczająco duży, aby zmieścić pełny token JSON, który jest wymagany dla czytelnika, aby kontynuować postęp w ładunku JSON. Przykład JSON podany w fragmencie kodu wyzwala wzrost rozmiaru buforu tylko wtedy, gdy ustawisz bardzo mały rozmiar buforu początkowego, na przykład 10 bajtów. W przypadku ustawienia początkowego rozmiaru buforu na 10, instrukcje Console.WriteLine ilustrują przyczyny i skutki zwiększenia rozmiaru buforu. Przy początkowym rozmiarze buforu o rozmiarze 4 KB cały przykładowy kod JSON jest wyświetlany przez każde wywołanie metody Console.WriteLine, a rozmiar buforu nigdy nie musi być zwiększany.

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

W poprzednim przykładzie nie określono limitu wielkości buforu. Jeśli rozmiar tokenu jest zbyt duży, kod może zakończyć się niepowodzeniem z wyjątkiem OutOfMemoryException. Może się tak zdarzyć, jeśli plik JSON zawiera token o rozmiarze około 1 GB lub większym, ponieważ podwojenie rozmiaru 1 GB powoduje, że rozmiar jest zbyt duży, aby zmieścić się w buforze int32 .

ograniczenia struktury ref

Utf8JsonReader Ponieważ typ to ref struct, ma pewne ograniczenia. Na przykład nie można go przechowywać jako pola w klasie lub strukturze innej niż ref struct.

Aby osiągnąć wysoką wydajność, Utf8JsonReader musi być typu ref struct, ponieważ musi buforować wejściowy ReadOnlySpan<byte> (który sam w sobie jest ref struct). Ponadto typ Utf8JsonReader jest modyfikowalny, ponieważ przechowuje stan. W związku z tym należy przekazać go przez odwołanie, a nie przez wartość. Przekazanie Utf8JsonReader przez wartość spowodowałoby skopiowanie struktury, a zmiany stanu nie byłyby widoczne dla elementu wywołującego.

Aby uzyskać więcej informacji na temat używania struktur ref, zobacz Unikanie alokacji.

Odczytywanie tekstu UTF-8

Aby uzyskać najlepszą wydajność podczas korzystania z Utf8JsonReader, odczytuj dane JSON już zakodowane jako tekst UTF-8, zamiast jako ciągi UTF-16. Aby zapoznać się z przykładem kodu, zobacz Filtrowanie danych przy użyciu Utf8JsonReader.

Odczyt za pomocą funkcji ReadOnlySequence z wieloma segmentami

Jeśli dane wejściowe JSON są bajtem <ReadOnlySpan>, dostęp do każdego elementu JSON można uzyskać z właściwości czytnika podczas przechodzenia przez pętlę odczytu. Jeśli jednak dane wejściowe są ReadOnlySequence<byte> (co jest wynikiem odczytu z PipeReader), niektóre elementy JSON mogą rozciągać się na wiele segmentów obiektu ReadOnlySequence<byte>. Te elementy nie byłyby dostępne w ValueSpan w ciągłym bloku pamięci. Zamiast tego za każdym razem, gdy masz wiele segmentów ReadOnlySequence<byte> jako dane wejściowe, sonduj HasValueSequence właściwość na czytniku, aby dowiedzieć się, jak uzyskać dostęp do bieżącego elementu JSON. Oto zalecany wzorzec:

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

Odczytywanie wielu dokumentów JSON

Na platformie .NET 9 i nowszych wersjach można odczytać wiele dokumentów JSON oddzielonych białymi znakami z jednego bufora lub strumienia. Domyślnie Utf8JsonReader zgłasza wyjątek, jeśli wykryje znaki inne niż białe znaki, które następują po pierwszym dokumencie na najwyższym poziomie. Można jednak skonfigurować to zachowanie przy użyciu flagi 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

Gdy AllowMultipleValues jest ustawiona na true, możesz również odczytywać dane JSON z ładunków zawierających końcowe dane, które nie są prawidłowe w formacie JSON.

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

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

Aby przesłać strumieniowo wiele wartości najwyższego poziomu, użyj przeciążenia DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) lub DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken). Domyślnie DeserializeAsyncEnumerable próbuje przesłać strumieniowo elementy, które znajdują się w pojedynczej tablicy JSON najwyższego poziomu. Przekaż topLevelValues dla parametru true, aby przesłać strumieniowo wiele wartości najwyższego poziomu.

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

Wyszukiwania nazw właściwości

Aby wyszukać nazwy właściwości, nie używaj ValueSpan do wykonywania porównań bajtów po bajcie poprzez wywołanie SequenceEqual. Zamiast tego wywołaj metodę ValueTextEquals, ponieważ ta metoda nie zawiera żadnych znaków, które zostały uniknięci w formacie JSON. Oto przykład pokazujący, jak wyszukać właściwość o nazwie "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;
    }
}

Odczytywanie wartości null do typów wartości obsługujących null

Wbudowane System.Text.Json interfejsy API zwracają tylko typy wartości, które nie mogą być null. Na przykład Utf8JsonReader.GetBoolean zwraca wartość bool. Zgłasza wyjątek, jeśli znajdzie Null w JSON. W poniższych przykładach przedstawiono dwa sposoby obsługi wartości null: jeden, poprzez zwrócenie typu wartości, który akceptuje null, a drugi, poprzez zwrócenie wartości domyślnej.

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

Pomiń elementy podrzędne tokenu

Użyj metody Utf8JsonReader.Skip(), aby pominąć elementy podrzędne bieżącego tokenu JSON. Jeśli typ tokenu to JsonTokenType.PropertyName, czytelnik przechodzi do wartości właściwości. Poniższy fragment kodu przedstawia przykład użycia Utf8JsonReader.Skip() do przeniesienia wskaźnika do wartości właściwości.

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

Używanie zdekodowanych ciągów JSON

Począwszy od platformy .NET 7, można użyć metody Utf8JsonReader.CopyString zamiast Utf8JsonReader.GetString(), aby przetwarzać zdekodowany ciąg JSON. W przeciwieństwie do GetString(), który zawsze przydziela nowy ciąg, CopyString umożliwia skopiowanie nieopanowanego ciągu do bufora, będącego w twoim posiadaniu. Poniższy fragment kodu przedstawia przykład korzystania z ciągu UTF-16 przy użyciu polecenia 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)
{
    // ...
}

Zobacz też