Använda Utf8JsonReader i System.Text.Json

Den här artikeln visar hur du kan använda typen Utf8JsonReader för att skapa anpassade parsers och deserializers.

Utf8JsonReader är en högpresterande, låg allokering, framåtriktad läsare för UTF-8-kodad JSON-text. Texten läss från en ReadOnlySpan<byte> eller ReadOnlySequence<byte>. Utf8JsonReader är en lågnivåtyp som kan användas för att skapa anpassade parsers och deserializers. (De JsonSerializer.Deserialize metoder som används Utf8JsonReader under täcket.)

I följande exempel visas hur du Utf8JsonReader använder klassen. Den här koden förutsätter att variabeln jsonUtf8Bytes är en bytematris som innehåller giltig JSON, kodad som 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

Kommentar

Utf8JsonReader kan inte användas direkt från Visual Basic-kod. Mer information finns i Visual Basic-stöd.

Filtrera data med hjälp av Utf8JsonReader

I följande exempel visas hur du synkront läser en fil och söker efter ett värde.

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

Koden ovan:

  • Förutsätter att JSON innehåller en matris med objekt och att varje objekt kan innehålla en namnegenskap av typen sträng.

  • Räknar objekt och egenskapsvärden för namn som slutar med "University".

  • Förutsätter att filen är kodad som UTF-16 och omkodar den till UTF-8.

    En fil som är kodad som UTF-8 kan läsas direkt i en ReadOnlySpan<byte> med hjälp av följande kod:

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

    Om filen innehåller en UTF-8 byte order mark (BOM) tar du bort den innan byte skickas till Utf8JsonReader, eftersom läsaren förväntar sig text. Annars anses bommen vara ogiltig JSON och läsaren genererar ett undantag.

Här är ett JSON-exempel som föregående kod kan läsa. Det resulterande sammanfattningsmeddelandet är "2 av 4 har namn som slutar med "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"
  }
]

Dricks

En asynkron version av det här exemplet finns i JSON-projekt med .NET-exempel.

Läsa från en dataström med hjälp av Utf8JsonReader

När du läser en stor fil (till exempel en gigabyte eller mer i storlek) kanske du vill undvika att läsa in hela filen i minnet på en gång. I det här scenariot kan du använda en FileStream.

När du använder Utf8JsonReader för att läsa från en ström gäller följande regler:

  • Bufferten som innehåller den partiella JSON-nyttolasten måste vara minst lika stor som den största JSON-token i den så att läsaren kan göra framsteg framåt.
  • Bufferten måste vara minst lika stor som den största sekvensen av tomt utrymme i JSON.
  • Läsaren håller inte reda på de data som den har läst förrän den helt läser nästa TokenType i JSON-nyttolasten. Så när det finns byte kvar i bufferten måste du skicka dem till läsaren igen. Du kan använda BytesConsumed för att avgöra hur många byte som är över.

Följande kod visar hur du läser från en dataström. Exemplet visar en MemoryStream. Liknande kod fungerar med en FileStream, förutom när innehåller FileStream en UTF-8-struktur i början. I så fall måste du ta bort dessa tre byte från bufferten innan du skickar de återstående byteen Utf8JsonReadertill . Annars skulle läsaren utlösa ett undantag eftersom bommen inte anses vara en giltig del av JSON.

Exempelkoden börjar med en buffert på 4 KB och fördubblar buffertstorleken varje gång den upptäcker att storleken inte är tillräckligt stor för att passa en fullständig JSON-token, vilket krävs för att läsaren ska kunna göra framsteg på JSON-nyttolasten. JSON-exemplet i kodfragmentet utlöser endast en ökning av buffertstorleken om du anger en mycket liten inledande buffertstorlek, till exempel 10 byte. Om du anger den inledande buffertstorleken Console.WriteLine till 10 illustrerar instruktionerna orsaken till och effekten av att buffertstorleken ökar. Vid den inledande buffertstorleken på 4 KB visas hela JSON-exemplet av varje anrop till Console.WriteLine, och buffertstorleken behöver aldrig ökas.

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

I föregående exempel anges ingen gräns för hur stor bufferten kan växa. Om tokenstorleken är för stor kan koden misslyckas med ett OutOfMemoryException undantag. Detta kan inträffa om JSON innehåller en token som är cirka 1 GB eller mer i storlek, eftersom en fördubbling av storleken på 1 GB resulterar i en storlek som är för stor för att få plats i en int32 buffert.

ref struct-begränsningar

Eftersom typen är en har den Utf8JsonReader vissa begränsningar.ref struct Det kan till exempel inte lagras som ett fält på en annan klass eller struct än en ref struct.

För att uppnå höga prestanda Utf8JsonReader måste vara en ref struct, eftersom den måste cachelagrar indata ReadOnlySpan<byte> (som i sig är en ref struct). Dessutom är typen Utf8JsonReader föränderlig eftersom den innehåller tillstånd. Skicka det därför med referens i stället för efter värde. Om värdet skickas Utf8JsonReader resulterar det i en struct-kopia och tillståndsändringarna visas inte för anroparen.

Mer information om hur du använder referensstrukturer finns i Undvik allokeringar.

Läsa UTF-8-text

För att uppnå bästa möjliga prestanda när du använder läser Utf8JsonReaderdu JSON-nyttolaster som redan kodas som UTF-8-text i stället för UTF-16-strängar. Ett kodexempel finns i Filtrera data med Utf8JsonReader.

Läsa med ReadOnlySequence i flera segment

Om JSON-indata är en ReadOnlySpan-byte<> kan varje JSON-element nås från ValueSpan egenskapen på läsaren när du går igenom läsloopen. Men om dina indata är en ReadOnlySequence-byte<> (vilket är resultatet av läsning från en PipeReader), kan vissa JSON-element korsa flera segment av ReadOnlySequence<byte> objektet. Dessa element skulle inte vara tillgängliga från ValueSpan i ett sammanhängande minnesblock. När du har ett flera segment ReadOnlySequence<byte> som indata avsöker HasValueSequence du i stället egenskapen på läsaren för att ta reda på hur du kommer åt det aktuella JSON-elementet. Här är ett rekommenderat mönster:

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

Läsa flera JSON-dokument

I .NET 9 och senare versioner kan du läsa flera, blankstegsavgränsade JSON-dokument från en enda buffert eller dataström. Som standard Utf8JsonReader utlöser ett undantag om det identifierar tecken som inte är blanksteg som följer det första dokumentet på den översta nivån. Du kan dock konfigurera det beteendet med hjälp av JsonReaderOptions.AllowMultipleValues flaggan.

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

När AllowMultipleValues är inställt på truekan du också läsa JSON från nyttolaster som innehåller avslutande data som är ogiltiga JSON.

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

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

Om du vill strömma flera värden på den översta nivån använder du DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) eller DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken) överlagrar. Som standard DeserializeAsyncEnumerable försöker strömma element som finns i en enda JSON-matris på den översta nivån. Skicka true för parametern topLevelValues för att strömma flera värden på den översta nivån.

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

Uppslag för egenskapsnamn

Om du vill söka efter egenskapsnamn använder ValueSpan du inte för att göra byte-för-byte-jämförelser genom att anropa SequenceEqual. Anropa ValueTextEqualsi stället , eftersom den här metoden tar bort alla tecken som är undantagna i JSON. Här är ett exempel som visar hur du söker efter en egenskap med namnet "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;
    }
}

Läsa null-värden i null-värdetyper

De inbyggda System.Text.Json API:erna returnerar endast icke-nullbara värdetyper. Returnerar Utf8JsonReader.GetBoolean till exempel en bool. Det genererar ett undantag om det hittar Null i JSON. I följande exempel visas två sätt att hantera nullvärden, ett genom att returnera en nullbar värdetyp och ett genom att returnera standardvärdet:

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

Hoppa över underordnade token

Använd metoden Utf8JsonReader.Skip() för att hoppa över underordnade till den aktuella JSON-token. Om tokentypen är JsonTokenType.PropertyNameflyttas läsaren till egenskapsvärdet. Följande kodfragment visar ett exempel på hur du använder Utf8JsonReader.Skip() för att flytta läsaren till värdet för en egenskap.

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

Använda avkodade JSON-strängar

Från och med .NET 7 kan du använda Utf8JsonReader.CopyString metoden i stället för att använda en avkodad Utf8JsonReader.GetString() JSON-sträng. Till skillnad från GetString(), som alltid allokerar en ny sträng, CopyString kan du kopiera den ej kapslade strängen till en buffert som du äger. Följande kodfragment visar ett exempel på hur du använder en UTF-16-sträng med .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)
{
    // ...
}

Se även