次の方法で共有


System.Text.Json で Utf8JsonReader を使う方法

この記事では、カスタム パーサーと逆シリアライザーを構築するために Utf8JsonReader 型を使う方法について説明します。

Utf8JsonReader は、UTF-8 でエンコードされた JSON テキスト用の高パフォーマンス、低割り当て、転送のみのリーダーです。 テキストは、ReadOnlySpan<byte> または ReadOnlySequence<byte> から読み取られます。 Utf8JsonReader は低レベルの型であり、カスタム パーサーとデシリアライザーを構築するために使用できます。 (JsonSerializer.Deserialize メソッドでは、内部で Utf8JsonReader が使用されます。)

Utf8JsonReader クラスを使用する方法を示す例を次に示します。 このコードでは、jsonUtf8Bytes 変数が、UTF-8 としてエンコードされた有効な JSON を含むバイト配列であることを想定しています。

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

Note

Utf8JsonReader は Visual Basic コードから直接使用することはできません。 詳細については、「Visual Basic のサポート」をご覧ください。

Utf8JsonReader を使用してデータをフィルター処理する

次の例は、同期的にファイルを読み取り、値を検索する方法を示しています。

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

上記のコードでは次の操作が行われます。

  • JSON にオブジェクトの配列が含まれ、各オブジェクトに文字列型の "name" プロパティが含まれている可能性があると想定します。

  • オブジェクトと "University" で終わる "name" プロパティ値をカウントします。

  • ファイルが UTF-16 としてエンコードされ、UTF-8 にトランスコードされるものと想定します。

    UTF-8 としてエンコードされたファイルは、次のコードを使用して、ReadOnlySpan<byte> に直接読み取ることができます。

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

    リーダーではテキストが想定されるため、ファイルに UTF-8 バイト オーダー マーク (BOM) が含まれている場合は、バイトを Utf8JsonReader に渡す前にそれを削除します。 そうしないと、BOM は無効な JSON と見なされ、リーダーによって例外がスローされます。

上記のコードで読み取ることができる JSON のサンプルを次に示します。 結果として生成される概要メッセージは、"2 out of 4 have names that end with '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"
  }
]

ヒント

この例の非同期バージョンについては、.NET サンプル JSON プロジェクトに関するページを参照してください。

Utf8JsonReader を使用してストリームから読み取る

大きなファイル (ギガバイト以上のサイズなど) を読み取る際に、ファイル全体を一度にメモリに読み込まない方がよい場合があります。 このシナリオでは、FileStream を使用できます。

Utf8JsonReader を使用してストリームから読み取る場合、次の規則が適用されます。

  • 部分的な JSON ペイロードが格納されるバッファーは、リーダーが処理を進めることができるように、少なくともその中で最大の JSON トークンと同じ大きさにする必要があります。
  • バッファーは、少なくとも JSON 内の空白の最大シーケンスと同じ大きさである必要があります。
  • リーダーでは、JSON ペイロード内の次の TokenType が完全に読み取られるまで、読み取られたデータが追跡されません。 そのため、バッファー内にバイトが残っている場合は、再びリーダーに渡す必要があります。 BytesConsumed を使用して、残っているバイト数を確認できます。

次のコードは、ストリームから読み取る方法を示しています。 この例は、MemoryStream を示しています。 同様のコードが FileStream で機能しますが、開始時に UTF-8 BOM が FileStream に含まれている場合を除きます。 その場合は、残りのバイトを Utf8JsonReader に渡す前に、バッファーからこれらの 3 バイトを取り除く必要があります。 そうしないと、BOM は JSON の有効な部分と見なされないため、リーダーによって例外がスローされます。

このサンプル コードでは、4 KB のバッファーから開始し、サイズが完全な JSON トークンに対応するのに十分な大きさではないことが判明するたびにバッファー サイズを 2 倍にします。これは、リーダーが JSON ペイロードの処理を進めるために必要です。 スニペットに用意されている JSON サンプルでは、非常に小さい初期バッファー サイズ (たとえば、10 バイト) を設定した場合にのみ、バッファー サイズが増加します。 初期バッファー サイズを 10 に設定すると、Console.WriteLine ステートメントによって、バッファー サイズの増加の原因と影響が示されます。 初期バッファー サイズが 4 KB で、Console.WriteLine の呼び出しごとにサンプル JSON 全体が表示されるため、バッファー サイズを増やす必要はありません。

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

前の例では、バッファーを拡大できる最大の大きさを無制限に設定しています。 トークン サイズが大きすぎる場合、コードは OutOfMemoryException 例外で失敗する可能性があります。 これは、JSON にサイズが約 1 GB 以上のトークンが含まれている場合に発生する可能性があります。1 GB のサイズを 2 倍にすると、サイズが大きすぎて int32 バッファーに入り切らないためです。

ref 構造体の制限事項

Utf8JsonReader 型は ref struct であるため、特定の制限があります。 たとえば、ref struct 以外のクラスまたは構造体にフィールドとして格納することはできません。

高いパフォーマンスを実現するには、Utf8JsonReaderref struct である必要があります。これは、入力の ReadOnlySpan<byte> (これ自体が ref struct です) をキャッシュする必要があるためです。 さらに、Utf8JsonReader 型は状態を保持するため変更可能です。 そのため、これは値ではなく参照渡しで渡してくださいUtf8JsonReader を値で渡すと、構造体のコピーが生成され、呼び出し元が状態の変更を確認できません。

ref 構造体の使用方法の詳細については、「割り当てを回避する」を参照してください。

UTF-8 テキストを読み取る

Utf8JsonReader を使用しているときに最大限のパフォーマンスを達成するには、UTF-16 文字列としてではなく、UTF-8 テキストとして既にエンコードされている JSON ペイロードを読み取ります。 コード例については、「Utf8JsonReader を使用してデータをフィルター処理する」を参照してください。

マルチセグメントの ReadOnlySequence で読み取る

JSON 入力が ReadOnlySpan<byte> の場合、各 JSON 要素には、読み取りループを実行するときにリーダーで ValueSpan プロパティからアクセスできます。 ただし、入力が ReadOnlySequence<byte> (これは、PipeReader からの読み取りの結果です) の場合、一部の JSON 要素が ReadOnlySequence<byte> オブジェクトの複数のセグメントにまたがることがあります。 これらの要素には、連続メモリ ブロック内で ValueSpan からアクセスすることはできません。 代わりに、マルチセグメントの ReadOnlySequence<byte> を入力として使用する場合は必ず、リーダーで HasValueSequence プロパティをポーリングして、現在の JSON 要素へのアクセス方法を確認します。 推奨されるパターンを次に示します。

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

複数の JSON ドキュメントを読み取る

.NET 9 以降のバージョンでは、単一のバッファーまたはストリームから、空白で区切られた複数の JSON ドキュメントを読み取ることができます。 既定では、最初の最上位ドキュメントの後に空白以外の文字が検出されると、Utf8JsonReader から例外がスローされます。 ただしこの動作は、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

AllowMultipleValuestrue に設定されている場合は、末尾データとして無効な JSON が含まれているペイロードからも JSON を読み取ることができます。

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

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

最上位レベルの値を複数ストリーミングするには、オーバーロード DeserializeAsyncEnumerable<TValue>(Stream, Boolean, JsonSerializerOptions, CancellationToken) または DeserializeAsyncEnumerable<TValue>(Stream, JsonTypeInfo<TValue>, Boolean, CancellationToken) を使用します。 既定では、DeserializeAsyncEnumerable は最上位レベルにある単一の JSON 配列に含まれる要素のストリーミングを試行します。 最上位レベルにある複数の値をストリーミングするには、topLevelValues パラメーターに true を渡します。

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

プロパティ名の参照

プロパティ名を検索するには、SequenceEqual を呼び出してバイト単位の比較を行う ValueSpan を使用しないでください。 代わりに ValueTextEquals を呼び出してください。このメソッドでは、JSON でエスケープされた文字がエスケープ解除されます。 以下に示すのは、"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;
    }
}

null 許容値型に null 値を読み込む

組み込みの System.Text.Json の API では、null 非許容値型のみが返されます。 たとえば、Utf8JsonReader.GetBoolean では bool が返されます。 JSON で Null が見つかると、例外がスローされます。 次の例は、null を処理する 2 つの方法を示しています。1 つは null 許容値型を返す方法で、もう 1 つは既定値を返す方法です。

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

トークンの子をスキップする

現在の JSON トークンの子のスキップするには、Utf8JsonReader.Skip() メソッドを使います。 トークンの種類が JsonTokenType.PropertyName である場合、リーダーはプロパティ値に移動します。 次のコード スニペットでは、Utf8JsonReader.Skip() を使ってリーダーをプロパティの値に移動する例を示します。

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

デコードされた JSON 文字列を使用する

.NET 7 以降では、デコードされた JSON 文字列を使用する場合に、Utf8JsonReader.GetString() の代わりに Utf8JsonReader.CopyString メソッドを使用できます。 常に新しい文字列を割り当てる GetString() とは異なり、CopyString ではエスケープされていない文字列を自分が所有するバッファにコピーできます。 次のコード スニペットは、CopyString を使用して UTF-16 文字列を使用する例を示しています。

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)
{
    // ...
}

こちらもご覧ください