Sdílet prostřednictvím


Jak používat Utf8JsonReader v System.Text.Json

Tento článek ukazuje, jak můžete použít Utf8JsonReader typ pro vytváření vlastních analyzátorů a deserializérů.

Utf8JsonReader je vysoce výkonná, nízká alokace, čtečka určená jen pro předávání textu JSON s kódováním UTF-8. Text se čte z nebo ReadOnlySpan<byte> ReadOnlySequence<byte>. Utf8JsonReader je typ nízké úrovně, který lze použít k vytváření vlastních analyzátorů a deserializérů. (Metody JsonSerializer.Deserialize používané Utf8JsonReader pod kryty.)

Následující příklad ukazuje, jak používat Utf8JsonReader třídu. Tento kód předpokládá, že jsonUtf8Bytes proměnná je bajtové pole, které obsahuje platný kód JSON kódovaný 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

Poznámka:

Utf8JsonReader nelze použít přímo z kódu jazyka Visual Basic. Další informace naleznete v tématu Podpora jazyka Visual Basic.

Filtrování dat pomocí Utf8JsonReader

Následující příklad ukazuje, jak synchronně číst soubor a hledat hodnotu.

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

Předchozí kód:

  • Předpokládá, že JSON obsahuje pole objektů a každý objekt může obsahovat vlastnost "name" typu string.

  • Počítá objekty a hodnoty vlastností name, které končí na "University".

  • Předpokládá, že soubor je kódovaný jako UTF-16 a překóduje ho do UTF-8.

    Soubor kódovaný jako UTF-8 lze číst přímo do souboru ReadOnlySpan<byte> pomocí následujícího kódu:

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

    Pokud soubor obsahuje značku pořadí bajtů UTF-8 (BOM), odeberte ho před předáním bajtů do objektu Utf8JsonReader, protože čtenář očekává text. V opačném případě se bom považuje za neplatný JSON a čtenář vyvolá výjimku.

Tady je ukázka JSON, kterou může předchozí kód přečíst. Výsledná souhrnná zpráva je "2 z 4 jmen, které končí na "Univerzita":

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

Tip

Asynchronní verzi tohoto příkladu najdete v ukázkách projektu JSON s ukázkami .NET.

Čtení ze streamu pomocí Utf8JsonReader

Při čtení velkého souboru (například gigabajtu nebo více), můžete se vyhnout načtení celého souboru do paměti najednou. Pro tento scénář můžete použít .FileStream

Při použití Utf8JsonReader čtení z datového proudu platí následující pravidla:

  • Vyrovnávací paměť obsahující částečnou datovou část JSON musí být alespoň tak velká jako největší token JSON v ní, aby čtenář mohl pokračovat.
  • Vyrovnávací paměť musí být alespoň tak velká jako největší posloupnost prázdných znaků v rámci JSON.
  • Čtenář nemá přehled o datech, která čte, dokud nepřečte další TokenType data v datové části JSON. Takže když v vyrovnávací paměti zůstanou bajty, musíte je znovu předat čtenáři. Můžete použít BytesConsumed k určení, kolik bajtů zbývá.

Následující kód ukazuje, jak číst z datového proudu. Příklad ukazuje .MemoryStream Podobný kód bude fungovat s FileStreamvýjimkou případů FileStream , kdy obsahuje kusovník UTF-8 na začátku. V takovém případě je nutné tyto tři bajty z vyrovnávací paměti odstranit před předáním zbývajících bajtů do Utf8JsonReader. V opačném případě by čtečka vyvolala výjimku, protože kusovník se nepovažuje za platnou část JSON.

Vzorový kód začíná vyrovnávací pamětí o velikosti 4 kB a pokaždé, když zjistí, že velikost není dostatečně velká, aby se vešla do kompletního tokenu JSON, který je nutný pro čtenáře, aby mohl pokračovat v datové části JSON. Ukázka JSON poskytnutá ve fragmentu kódu aktivuje zvětšení velikosti vyrovnávací paměti pouze v případě, že nastavíte velmi malou počáteční velikost vyrovnávací paměti, například 10 bajtů. Pokud nastavíte počáteční velikost vyrovnávací paměti na 10, Console.WriteLine příkazy znázorňují příčinu a vliv zvětšení velikosti vyrovnávací paměti. Při počáteční velikosti vyrovnávací paměti 4 kB se při každém volání Console.WriteLinezobrazí celý ukázkový kód JSON a velikost vyrovnávací paměti se nikdy nemusí zvětšit.

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

Předchozí příklad nenastavuje žádné omezení, jak velká vyrovnávací paměť může růst. Pokud je velikost tokenu příliš velká, může kód selhat s OutOfMemoryException výjimkou. K tomu může dojít v případě, že JSON obsahuje token, který má velikost přibližně 1 GB nebo větší, protože při zdvojnásobení velikosti 1 GB dojde k příliš velké velikosti, aby se vešla do int32 vyrovnávací paměti.

omezení struktury ref

Vzhledem k tomu, že Utf8JsonReader typ je , ref structurčitá omezení. Nelze ji například uložit jako pole v jiné třídě nebo struktuře než ref struct.

Aby bylo možné dosáhnout vysokého výkonu, Utf8JsonReader musí být , ref structprotože musí ukládat vstupní bajt> ReadOnlySpan<(což je ref structsám ). Kromě toho je typ proměnlivý, Utf8JsonReader protože obsahuje stav. Proto ji předejte odkazem místo podle hodnoty. Utf8JsonReader Předáním hodnoty by došlo k zkopírování struktury a změny stavu by volajícímu nebylo viditelné.

Další informace o tom, jak používat ref struktury, naleznete v tématu Vyhněte se přidělení.

Čtení textu UTF-8

Pokud chcete dosáhnout nejlepšího možného výkonu při použití Utf8JsonReader, přečtěte si datové části JSON, které jsou už kódované jako text UTF-8, a ne jako řetězce UTF-16. Příklad kódu naleznete v tématu Filtrování dat pomocí Utf8JsonReader.

Čtení pomocí funkce ReadOnlySequence s více segmenty

Pokud je vstupEM JSON bajt> ReadOnlySpan<, každý prvek JSON je přístupný z ValueSpan vlastnosti čtenáře při procházení smyčky čtení. Pokud je ale vaším vstupem bajt> ReadOnlySequence<(což je výsledek čtení z aPipeReader), můžou některé prvky JSON řadit více segmentů objektu ReadOnlySequence<byte> . Tyto prvky by nebyly přístupné z ValueSpan souvislého bloku paměti. Místo toho pokaždé, když jako vstup máte více segmentů ReadOnlySequence<byte> , dotazujte HasValueSequence se na vlastnost čtenáře a zjistěte, jak získat přístup k aktuálnímu prvku JSON. Tady je doporučený vzor:

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

Čtení více dokumentů JSON

V .NET 9 a novějších verzích můžete číst více dokumentů JSON oddělených prázdnými znaky z jedné vyrovnávací paměti nebo datového proudu. Ve výchozím nastavení vyvolá výjimku, pokud zjistí všechny znaky, které nejsou prázdné znaky, Utf8JsonReader které na konci prvního dokumentu nejvyšší úrovně. Toto chování ale můžete nakonfigurovat pomocí příznaku 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

Pokud AllowMultipleValues je nastavená hodnota true, můžete také číst JSON z datových částí, které obsahují koncová data, která jsou neplatná.

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

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

Pokud chcete streamovat více hodnot nejvyšší úrovně, použijte DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) hodnotu nebo DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken) přetížení. Ve výchozím nastavení DeserializeAsyncEnumerable se pokusí streamovat prvky, které jsou obsaženy v jednom poli JSON nejvyšší úrovně. Předání true parametru topLevelValues pro streamování více hodnot nejvyšší úrovně.

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

Vyhledávání názvů vlastností

Pokud chcete vyhledat názvy vlastností, nepoužívejte ValueSpan k porovnávání bajtů bajty voláním SequenceEqual. Místo toho volejte ValueTextEquals, protože tato metoda unescapes všechny znaky, které jsou řídicími znaky ve formátu JSON. Tady je příklad, který ukazuje, jak vyhledat vlastnost s názvem "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;
    }
}

Čtení hodnot null do typů hodnot s možnou hodnotou null

Integrovaná System.Text.Json rozhraní API vrací pouze typy hodnot, které nemají hodnotu null. Například Utf8JsonReader.GetBoolean vrátí hodnotu bool. Vyvolá výjimku, pokud ji najde Null ve formátu JSON. Následující příklady ukazují dva způsoby zpracování hodnot null, jeden vrácením typu hodnoty s možnou hodnotou null a druhý vrácením výchozí hodnoty:

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

Přeskočit podřízené položky tokenu

Utf8JsonReader.Skip() Pomocí metody můžete přeskočit podřízené položky aktuálního tokenu JSON. Pokud je JsonTokenType.PropertyNametyp tokenu , čtenář se přesune na hodnotu vlastnosti. Následující fragment kódu ukazuje příklad použití Utf8JsonReader.Skip() k přesunutí čtenáře na hodnotu vlastnosti.

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

Využívání dekódovaných řetězců JSON

Počínaje rozhraním .NET 7 můžete místo použití dekódovaného řetězce JSON použít Utf8JsonReader.CopyString metodu Utf8JsonReader.GetString() . Na rozdíl od GetString()toho, který vždy přidělí nový řetězec, CopyString umožňuje zkopírovat unescaped řetězec do vyrovnávací paměti, kterou vlastníte. Následující fragment kódu ukazuje příklad využívání řetězce UTF-16 pomocí 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)
{
    // ...
}

Viz také