System.Text.Json で JSON ドキュメント、Utf8JsonReader、Utf8JsonWriter を使用する方法

この記事では、次を使用する方法について説明します。

JSON DOM の選択肢

DOM を使用することは、JsonSerializer による逆シリアル化の代替手段です。

  • 逆シリアル化する型がない場合。
  • 受信した JSON に固定スキーマがなく、含まれている内容を確認するために検査する必要がある場合。

System.Text.Json には、JSON DOM を構築する 2 つの方法が用意されています。

  • JsonDocument を使用すると、Utf8JsonReader を使用して読み取り専用 DOM を構築することができます。 ペイロードを構成する JSON 要素には、JsonElement 型を使用してアクセスできます。 JsonElement 型では、配列とオブジェクト列挙子と共に、JSON テキストを一般的な .NET 型に変換する API が提供されます。 JsonDocument では RootElement プロパティが公開されます。 詳細については、この記事で後述する「JsonDocument の使用」を参照してください。

JsonDocumentJsonNode のどちらを使用するか選ぶときは、次の要素を考慮してください。

  • JsonNode DOM は作成後に変更できます。 JsonDocument DOM は変更できません。
  • JsonDocument DOM では、そのデータにより高速にアクセスできます。

JsonNode を使用します

以下の例では、System.Text.Json.Nodes 名前空間の JsonNode と他の型を使用して、次の操作を行う方法を示します。

  • JSON 文字列から DOM を作成する
  • DOM から JSON を書き込む。
  • DOM から値、オブジェクト、または配列を取得する。
using System;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeFromStringExample
{
    public class Program
    {
        public static void Main()
        {
            string jsonString =
@"{
  ""Date"": ""2019-08-01T00:00:00"",
  ""Temperature"": 25,
  ""Summary"": ""Hot"",
  ""DatesAvailable"": [
    ""2019-08-01T00:00:00"",
    ""2019-08-02T00:00:00""
  ],
  ""TemperatureRanges"": {
      ""Cold"": {
          ""High"": 20,
          ""Low"": -10
      },
      ""Hot"": {
          ""High"": 60,
          ""Low"": 20
      }
  }
}
";
            // Create a JsonNode DOM from a JSON string.
            JsonNode forecastNode = JsonNode.Parse(jsonString);

            // Write JSON from a JsonNode
            var options = new JsonSerializerOptions { WriteIndented = true };
            Console.WriteLine(forecastNode.ToJsonString(options));
            // output:
            //{
            //  "Date": "2019-08-01T00:00:00",
            //  "Temperature": 25,
            //  "Summary": "Hot",
            //  "DatesAvailable": [
            //    "2019-08-01T00:00:00",
            //    "2019-08-02T00:00:00"
            //  ],
            //  "TemperatureRanges": {
            //    "Cold": {
            //      "High": 20,
            //      "Low": -10
            //    },
            //    "Hot": {
            //      "High": 60,
            //      "Low": 20
            //    }
            //  }
            //}

            // Get value from a JsonNode.
            JsonNode temperatureNode = forecastNode["Temperature"];
            Console.WriteLine($"Type={temperatureNode.GetType()}");
            Console.WriteLine($"JSON={temperatureNode.ToJsonString()}");
            //output:
            //Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
            //JSON = 25

            // Get a typed value from a JsonNode.
            int temperatureInt = (int)forecastNode["Temperature"];
            Console.WriteLine($"Value={temperatureInt}");
            //output:
            //Value=25

            // Get a typed value from a JsonNode by using GetValue<T>.
            temperatureInt = forecastNode["Temperature"].GetValue<int>();
            Console.WriteLine($"TemperatureInt={temperatureInt}");
            //output:
            //Value=25

            // Get a JSON object from a JsonNode.
            JsonNode temperatureRanges = forecastNode["TemperatureRanges"];
            Console.WriteLine($"Type={temperatureRanges.GetType()}");
            Console.WriteLine($"JSON={temperatureRanges.ToJsonString()}");
            //output:
            //Type = System.Text.Json.Nodes.JsonObject
            //JSON = { "Cold":{ "High":20,"Low":-10},"Hot":{ "High":60,"Low":20} }

            // Get a JSON array from a JsonNode.
            JsonNode datesAvailable = forecastNode["DatesAvailable"];
            Console.WriteLine($"Type={datesAvailable.GetType()}");
            Console.WriteLine($"JSON={datesAvailable.ToJsonString()}");
            //output:
            //datesAvailable Type = System.Text.Json.Nodes.JsonArray
            //datesAvailable JSON =["2019-08-01T00:00:00", "2019-08-02T00:00:00"]

            // Get an array element value from a JsonArray.
            JsonNode firstDateAvailable = datesAvailable[0];
            Console.WriteLine($"Type={firstDateAvailable.GetType()}");
            Console.WriteLine($"JSON={firstDateAvailable.ToJsonString()}");
            //output:
            //Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
            //JSON = "2019-08-01T00:00:00"

            // Get a typed value by chaining references.
            int coldHighTemperature = (int)forecastNode["TemperatureRanges"]["Cold"]["High"];
            Console.WriteLine($"TemperatureRanges.Cold.High={coldHighTemperature}");
            //output:
            //TemperatureRanges.Cold.High = 20

            // Parse a JSON array
            var datesNode = JsonNode.Parse(@"[""2019-08-01T00:00:00"",""2019-08-02T00:00:00""]");
            JsonNode firstDate = datesNode[0].GetValue<DateTime>();
            Console.WriteLine($"firstDate={ firstDate}");
            //output:
            //firstDate = "2019-08-01T00:00:00"
        }
    }
}

オブジェクト初期化子を使用して JsonNode DOM を作成し、変更を行う

以下の例では、次のことを行っています。

  • オブジェクト初期化子を使用して DOM を作成する。
  • DOM に変更を加える。
using System;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeFromObjectExample
{
    public class Program
    {
        public static void Main()
        {
            // Create a new JsonObject using object initializers.
            var forecastObject = new JsonObject
            {
                ["Date"] = new DateTime(2019, 8, 1),
                ["Temperature"] = 25,
                ["Summary"] = "Hot",
                ["DatesAvailable"] = new JsonArray(
                    new DateTime(2019, 8, 1), new DateTime(2019, 8, 2)),
                ["TemperatureRanges"] = new JsonObject
                {
                    ["Cold"] = new JsonObject
                    {
                        ["High"] = 20,
                        ["Low"] = -10
                    }
                },
                ["SummaryWords"] = new JsonArray("Cool", "Windy", "Humid")
            };

            // Add an object.
            forecastObject["TemperatureRanges"]["Hot"] =
                new JsonObject { ["High"] = 60, ["Low"] = 20 };

            // Remove a property.
            forecastObject.Remove("SummaryWords");

            // Change the value of a property.
            forecastObject["Date"] = new DateTime(2019, 8, 3);

            var options = new JsonSerializerOptions { WriteIndented = true };
            Console.WriteLine(forecastObject.ToJsonString(options));
            //output:
            //{
            //  "Date": "2019-08-03T00:00:00",
            //  "Temperature": 25,
            //  "Summary": "Hot",
            //  "DatesAvailable": [
            //    "2019-08-01T00:00:00",
            //    "2019-08-02T00:00:00"
            //  ],
            //  "TemperatureRanges": {
            //    "Cold": {
            //      "High": 20,
            //      "Low": -10
            //    },
            //    "Hot": {
            //      "High": 60,
            //      "Low": 20
            //    }
            //  }
            //}
        }
    }
}

JSON ペイロードのサブセクションを逆シリアル化する

次の例では、JsonNode を使用して、JSON ツリーのサブセクションに移動し、そのサブセクションからの単一の値、カスタム型、または配列を逆シリアル化する方法を示しています。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodePOCOExample
{
    public class TemperatureRanges : Dictionary<string, HighLowTemps>
    {
    }

    public class HighLowTemps
    {
        public int High { get; set; }
        public int Low { get; set; }
    }

    public class Program
    {
        public static DateTime[] DatesAvailable { get; set; }

        public static void Main()
        {
            string jsonString =
 @"{
  ""Date"": ""2019-08-01T00:00:00"",
  ""Temperature"": 25,
  ""Summary"": ""Hot"",
  ""DatesAvailable"": [
    ""2019-08-01T00:00:00"",
    ""2019-08-02T00:00:00""
  ],
  ""TemperatureRanges"": {
      ""Cold"": {
          ""High"": 20,
          ""Low"": -10
      },
      ""Hot"": {
          ""High"": 60,
          ""Low"": 20
      }
  }
}
";
            // Parse all of the JSON.
            JsonNode forecastNode = JsonNode.Parse(jsonString);

            // Get a single value
            int hotHigh = forecastNode["TemperatureRanges"]["Hot"]["High"].GetValue<int>();
            Console.WriteLine($"Hot.High={hotHigh}");
            // output:
            //Hot.High=60

            // Get a subsection and deserialize it into a custom type.
            JsonObject temperatureRangesObject = forecastNode["TemperatureRanges"].AsObject();
            using var stream = new MemoryStream();
            using var writer = new Utf8JsonWriter(stream);
            temperatureRangesObject.WriteTo(writer);
            writer.Flush();
            TemperatureRanges temperatureRanges = 
                JsonSerializer.Deserialize<TemperatureRanges>(stream.ToArray());
            Console.WriteLine($"Cold.Low={temperatureRanges["Cold"].Low}, Hot.High={temperatureRanges["Hot"].High}");
            // output:
            //Cold.Low=-10, Hot.High=60

            // Get a subsection and deserialize it into an array.
            JsonArray datesAvailable = forecastNode["DatesAvailable"].AsArray();
            Console.WriteLine($"DatesAvailable[0]={datesAvailable[0]}");
            // output:
            //DatesAvailable[0]=8/1/2019 12:00:00 AM
        }
    }
}

JsonNode の平均グレードの例

次の例では、整数値を持つ JSON 配列を選択し、平均値を計算します。

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeAverageGradeExample
{
    public class Program
    {
        public static void Main()
        {
            string jsonString =
 @"{
  ""Class Name"": ""Science"",
  ""Teacher\u0027s Name"": ""Jane"",
  ""Semester"": ""2019-01-01"",
  ""Students"": [
    {
      ""Name"": ""John"",
      ""Grade"": 94.3
    },
    {
      ""Name"": ""James"",
      ""Grade"": 81.0
    },
    {
      ""Name"": ""Julia"",
      ""Grade"": 91.9
    },
    {
      ""Name"": ""Jessica"",
      ""Grade"": 72.4
    },
    {
      ""Name"": ""Johnathan""
    }
  ],
  ""Final"": true
}
";
            double sum = 0;
            int count = 0;

            JsonNode document = JsonNode.Parse(jsonString);

            JsonNode root = document.Root;
            JsonArray studentsArray = root["Students"].AsArray();

            count = studentsArray.Count;

            foreach (JsonNode student in studentsArray)
            {
                if (student["Grade"] is JsonNode gradeNode)
                {
                    sum += (double)gradeNode;
                }
                else
                {
                    sum += 70;
                }
            }

            double average = sum / count;
            Console.WriteLine($"Average grade : {average}");
        }
    }
}
// output:
//Average grade : 81.92

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

  • Grade プロパティを持つ Students 配列内のオブジェクトの平均グレードを計算します。
  • グレードのない学生には既定のグレード 70 を割り当てます。
  • JsonArrayCount プロパティから学生の数を取得します。

JsonDocument を使用します

次の例は、JsonDocument クラスを使用して JSON 文字列内のデータにランダム アクセスする方法を示しています。

double sum = 0;
int count = 0;

using (JsonDocument document = JsonDocument.Parse(jsonString))
{
    JsonElement root = document.RootElement;
    JsonElement studentsElement = root.GetProperty("Students");
    foreach (JsonElement student in studentsElement.EnumerateArray())
    {
        if (student.TryGetProperty("Grade", out JsonElement gradeElement))
        {
            sum += gradeElement.GetDouble();
        }
        else
        {
            sum += 70;
        }
        count++;
    }
}

double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
    Dim root As JsonElement = document.RootElement
    Dim studentsElement As JsonElement = root.GetProperty("Students")
    For Each student As JsonElement In studentsElement.EnumerateArray()
        Dim gradeElement As JsonElement = Nothing
        If student.TryGetProperty("Grade", gradeElement) Then
            sum += gradeElement.GetDouble()
        Else
            sum += 70
        End If
        count += 1
    Next
End Using

Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")

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

  • 分析する JSON が jsonString という名前の文字列に含まれていると想定します。
  • Grade プロパティを持つ Students 配列内のオブジェクトの平均グレードを計算します。
  • グレードのない学生には既定のグレード 70 を割り当てます。
  • JsonDocument によって IDisposable が実装されるため、using ステートメントJsonDocument インスタンスを作成します。 JsonDocumentインスタンスが破棄されると、そのすべての JsonElement インスタンスにもアクセスできなくなります。 JsonElement インスタンスへのアクセスを保持するには、親 JsonDocument インスタンスが破棄される前に、そのコピーを作成します。 コピーを作成するには、JsonElement.Clone を呼び出します。 詳細については、「JsonDocument は IDisposable」を参照してください。

前のコード例では、反復処理ごとに count 変数をインクリメントして学生をカウントします。 別の方法として、次の例に示すように GetArrayLength を呼び出すこともできます。

double sum = 0;
int count = 0;

using (JsonDocument document = JsonDocument.Parse(jsonString))
{
    JsonElement root = document.RootElement;
    JsonElement studentsElement = root.GetProperty("Students");

    count = studentsElement.GetArrayLength();

    foreach (JsonElement student in studentsElement.EnumerateArray())
    {
        if (student.TryGetProperty("Grade", out JsonElement gradeElement))
        {
            sum += gradeElement.GetDouble();
        }
        else
        {
            sum += 70;
        }
    }
}

double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
    Dim root As JsonElement = document.RootElement
    Dim studentsElement As JsonElement = root.GetProperty("Students")

    count = studentsElement.GetArrayLength()

    For Each student As JsonElement In studentsElement.EnumerateArray()
        Dim gradeElement As JsonElement = Nothing
        If student.TryGetProperty("Grade", gradeElement) Then
            sum += gradeElement.GetDouble()
        Else
            sum += 70
        End If
    Next
End Using

Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")

このコードで処理される JSON の例を次に示します。

{
  "Class Name": "Science",
  "Teacher\u0027s Name": "Jane",
  "Semester": "2019-01-01",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ],
  "Final": true
}

JsonDocument の代わりに JsonNode を使用する同様の例については、「JsonNode の平均グレードの例」を参照してください。

JsonDocument と JsonElement でのサブ要素の検索方法

JsonElement での検索にはプロパティの順次検索が必要になるため、比較的低速になります (たとえば、TryGetProperty を使用する場合)。 System.Text.Json は、検索時間ではなく、初期解析時間を最小限に抑えるように設計されています。 そのため、JsonDocument オブジェクトで検索する場合は、次の方法を使用してパフォーマンスを最適化してください。

  • 独自のインデックス作成やループを実行するのではなく、組み込みの列挙子 (EnumerateArrayEnumerateObject) を使用します。
  • RootElement を使用して、JsonDocument 全体ですべてのプロパティの順次検索を行わないでください。 代わりに、JSON データの既知の構造に基づいて、入れ子になった JSON オブジェクトで検索します。 たとえば上記のコード例では、Student オブジェクトをループし、それぞれの Grade の値を取得して、Student オブジェクトで Grade プロパティを探します。すべての JsonElement オブジェクトで Grade プロパティを検索することは行いません。 後者を行うと、同じデータに対して不要なパスが行われます。

JsonDocument を使用して JSON を書き込む

次の例では、JsonDocument から JSON を書き込む方法を示します。

string jsonString = File.ReadAllText(inputFileName);

var writerOptions = new JsonWriterOptions
{
    Indented = true
};

var documentOptions = new JsonDocumentOptions
{
    CommentHandling = JsonCommentHandling.Skip
};

using FileStream fs = File.Create(outputFileName);
using var writer = new Utf8JsonWriter(fs, options: writerOptions);
using JsonDocument document = JsonDocument.Parse(jsonString, documentOptions);

JsonElement root = document.RootElement;

if (root.ValueKind == JsonValueKind.Object)
{
    writer.WriteStartObject();
}
else
{
    return;
}

foreach (JsonProperty property in root.EnumerateObject())
{
    property.WriteTo(writer);
}

writer.WriteEndObject();

writer.Flush();
Dim jsonString As String = File.ReadAllText(inputFileName)

Dim writerOptions As JsonWriterOptions = New JsonWriterOptions With {
    .Indented = True
}

Dim documentOptions As JsonDocumentOptions = New JsonDocumentOptions With {
    .CommentHandling = JsonCommentHandling.Skip
}

Dim fs As FileStream = File.Create(outputFileName)
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(fs, options:=writerOptions)
Dim document As JsonDocument = JsonDocument.Parse(jsonString, documentOptions)

Dim root As JsonElement = document.RootElement

If root.ValueKind = JsonValueKind.[Object] Then
    writer.WriteStartObject()
Else
    Return
End If

For Each [property] As JsonProperty In root.EnumerateObject()
    [property].WriteTo(writer)
Next

writer.WriteEndObject()

writer.Flush()

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

  • JSON ファイルを読み取り、データを JsonDocument に読み込み、書式設定された (整形された) JSON をファイルに書き込みます。
  • JsonDocumentOptions を使用して、入力 JSON 内でコメントは許可されるが無視されることを指定します。
  • 完了したら、ライターに対して Flush を呼び出します。 別の方法として、破棄されたときにライターを自動フラッシュすることもできます。

コード例によって処理される JSON 入力の例を次に示します。

{"Class Name": "Science","Teacher's Name": "Jane","Semester": "2019-01-01","Students": [{"Name": "John","Grade": 94.3},{"Name": "James","Grade": 81.0},{"Name": "Julia","Grade": 91.9},{"Name": "Jessica","Grade": 72.4},{"Name": "Johnathan"}],"Final": true}

その結果は、次のような整形された JSON 出力になります。

{
  "Class Name": "Science",
  "Teacher\u0027s Name": "Jane",
  "Semester": "2019-01-01",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ],
  "Final": true
}

JsonDocument は IDisposable

JsonDocument では、データのメモリ内ビューがプールされたバッファー内に作成されます。 そのため、JsonDocument 型は IDisposable を実装し、using ブロック内で使用される必要があります。

有効期間中全体の所有権を呼び出し元に移譲し、責任を破棄する場合は、API から JsonDocument のみを返します。 ほとんどのシナリオでは、これは必要ありません。 呼び出し元が JSON ドキュメント全体を操作する必要がある場合は、RootElement (つまり JsonElement) の Clone を返します。 呼び出し元が JSON ドキュメント内の特定の要素を操作する必要がある場合は、その JsonElementClone を返します。 Clone を作成せずに直接 RootElement またはサブ要素を返した場合、呼び出し元は、返された JsonElement には、それを所有する JsonDocument が破棄されるとアクセスできなくなります。

Clone を作成する必要がある例を次に示します。

public JsonElement LookAndLoad(JsonElement source)
{
    string json = File.ReadAllText(source.GetProperty("fileName").GetString());

    using (JsonDocument doc = JsonDocument.Parse(json))
    {
        return doc.RootElement.Clone();
    }
}

上記のコードでは、fileName プロパティを含む JsonElement が想定されています。 これにより、JSON ファイルが開き、JsonDocument が作成されます。 このメソッドでは、呼び出し元がドキュメント全体を操作することが想定されているため、RootElementClone が返されます。

JsonElement を受け取り、サブ要素を返す場合は、サブ要素の Clone を返す必要はありません。 呼び出し元は、渡された JsonElement が属している JsonDocument が破棄されないように維持する役割を負っています。 次に例を示します。

public JsonElement ReturnFileName(JsonElement source)
{
   return source.GetProperty("fileName");
}

Utf8JsonWriter を使用します

Utf8JsonWriter は、StringInt32DateTime のような一般的な .NET 型から UTF-8 でエンコードされた JSON テキストを書き込むための、ハイパフォーマンスな方法です。 ライターは低レベルの型であり、カスタム シリアライザーを構築するために使用できます。 JsonSerializer.Serialize メソッドでは、内部で Utf8JsonWriter が使用されます。

Utf8JsonWriter クラスを使用する方法を示す例を次に示します。

var options = new JsonWriterOptions
{
    Indented = true
};

using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);

writer.WriteStartObject();
writer.WriteString("date", DateTimeOffset.UtcNow);
writer.WriteNumber("temp", 42);
writer.WriteEndObject();
writer.Flush();

string json = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
Dim options As JsonWriterOptions = New JsonWriterOptions With {
    .Indented = True
}

Dim stream As MemoryStream = New MemoryStream
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(stream, options)

writer.WriteStartObject()
writer.WriteString("date", DateTimeOffset.UtcNow)
writer.WriteNumber("temp", 42)
writer.WriteEndObject()
writer.Flush()

Dim json As String = Encoding.UTF8.GetString(stream.ToArray())
Console.WriteLine(json)

UTF-8 テキストで書き込む

Utf8JsonWriter を使用しているときに最大限のパフォーマンスを達成するには、UTF-16 文字列としてではなく、UTF-8 テキストとして既にエンコードされている JSON ペイロードを書き込みます。 UTF-16 文字列リテラルを使用するのではなく、JsonEncodedText を使用して、既知の文字列プロパティの名前と値をスタティックとしてキャッシュおよび事前エンコードし、ライターに渡します。 これは、UTF-8 バイト配列をキャッシュして使用するより高速です。

この方法は、カスタム エスケープ処理を行う必要がある場合にも機能します。 System.Text.Json では、文字列の記述中にエスケープ処理を無効にすることはできません。 ただし、独自のカスタム JavaScriptEncoder をオプションとしてライターに渡すことができます。または、JavascriptEncoder を使用する独自の JsonEncodedText を作成してエスケープ処理を行ってから、文字列の代わりに JsonEncodedText を書き込むこともできます。 詳細については、「文字エンコードをカスタマイズする」を参照してください。

生 JSON の書き込み

一部のシナリオでは、Utf8JsonWriter を使用して作成している JSON ペイロードに "未加工の" JSON を書き込む場合があります。 これは Utf8JsonWriter.WriteRawValue を使用して行うことができます。 典型的なシナリオは次のようになります。

  • 新しい JSON で囲む既存の JSON ペイロードがあります。

  • Utf8JsonWriter の既定の書式設定とは異なる方法で値の書式を設定します。

    たとえば、数値の書式設定をカスタマイズします。 既定では、System.Text.Json では全数の小数点が省略されます。たとえば、1.0 ではなく 1 と書きます。 その理由は、書き込むバイト数が少なければ少ないほどパフォーマンスが向上することにあります。 ただし、JSON を使用する人が小数のある数字を倍精度として、小数のない数字を整数として扱うとします。 そこで、配列内の数字がすべて倍精度として認識されるように、全数には小数点とゼロを書きます。 その方法を次の例に示します。

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Text.Json;
    using System.Threading.Tasks;
    
    namespace WriteRawJson
    {
        public class Program
        {
            public static void Main()
            {
                JsonWriterOptions writerOptions = new() { Indented = true, };
    
                using MemoryStream stream = new();
                using Utf8JsonWriter writer = new(stream, writerOptions);
    
                writer.WriteStartObject();
    
                writer.WriteStartArray("defaultJsonFormatting");
                foreach (double number in new double[] { 50.4, 51 })
                {
                    writer.WriteStartObject();
                    writer.WritePropertyName("value");
                    writer.WriteNumberValue(number);
                    writer.WriteEndObject();
                }
                writer.WriteEndArray();
    
                writer.WriteStartArray("customJsonFormatting");
                foreach (double result in new double[] { 50.4, 51 })
                {
                    writer.WriteStartObject();
                    writer.WritePropertyName("value");
                    writer.WriteRawValue(
                        FormatNumberValue(result), skipInputValidation: true);
                    writer.WriteEndObject();
                }
                writer.WriteEndArray();
    
                writer.WriteEndObject();
                writer.Flush();
    
                string json = Encoding.UTF8.GetString(stream.ToArray());
                Console.WriteLine(json);
            }
            static string FormatNumberValue(double numberValue)
            {
                return numberValue == Convert.ToInt32(numberValue) ? 
                    numberValue.ToString() + ".0" : numberValue.ToString();
            }
        }
    }
    // output:
    //{
    //  "defaultJsonFormatting": [
    //    {
    //      "value": 50.4
    //    },
    //    {
    //      "value": 51
    //    }
    //  ],
    //  "customJsonFormatting": [
    //    {
    //      "value": 50.4
    //    },
    //    {
    //      "value": 51.0
    //    }
    //  ]
    //}
    

文字のエスケープ処理をカスタマイズする

JsonTextWriterStringEscapeHandling 設定には、すべての ASCII 以外の文字 または HTML 文字をエスケープするオプションが用意されています。 既定では、Utf8JsonWriter ではすべての ASCII 以外 および HTML 文字がエスケープされます。 このエスケープ処理は、多層防御セキュリティ上の理由で行われます。 別のエスケープ処理ポリシーを指定するには、JavaScriptEncoder を作成し、JsonWriterOptions.Encoder を設定します。 詳細については、「文字エンコードをカスタマイズする」を参照してください。

null 値を書き込む

Utf8JsonWriter を使用して null 値を書き込むには、以下を呼び出します。

  • WriteNull。null を値として指定し、キーと値のペアを書き込みます。
  • WriteNullValue。JSON 配列の要素として null を書き込みます。

文字列プロパティでは、文字列が null の場合、WriteStringWriteStringValueWriteNullWriteNullValue に相当します。

Timespan、Uri、または char の値を書き込む

TimespanUri、または char の値を書き込むには、これらを文字列として書式設定し (たとえば ToString() を呼び出します)、WriteStringValue を呼び出します。

Utf8JsonReader を使用します

Utf8JsonReader は、UTF-8 でエンコードされた JSON テキスト用の、ハイパフォーマンス、低割り当て、順方向専用のリーダーです。ReadOnlySpan<byte> または ReadOnlySequence<byte> から読み取られます。 Utf8JsonReader は低レベルの型であり、カスタム パーサーとデシリアライザーを構築するために使用できます。 JsonSerializer.Deserialize メソッドでは、内部で Utf8JsonReader が使用されます。 Utf8JsonReader は Visual Basic コードから直接使用することはできません。 詳細については、「Visual Basic のサポート」をご覧ください。

Utf8JsonReader クラスを使用する方法を示す例を次に示します。

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://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

上記のコードでは、jsonUtf8 変数が、UTF-8 としてエンコードされた有効な JSON を含むバイト配列であることを想定しています。

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

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

using System;
using System.IO;
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://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

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

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

  • 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"
  }
]

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 の初期バッファー サイズで、サンプルの JSON 全体が各 Console.WriteLine によって表示され、バッファー サイズを増やす必要はありません。

using System;
using System.IO;
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://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

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

Utf8JsonReader は ref 構造体

Utf8JsonReader 型は "ref 構造体" であるため、特定の制限があります。 たとえば、ref 構造体以外のクラスまたは構造体にフィールドとして格納することはできません。 ハイ パフォーマンスを実現するには、この型が ref struct である必要があります。これは、入力の ReadOnlySpan<byte> (これ自体が ref 構造体です) をキャッシュする必要があるためです。 さらに、この型は状態が保持されるため変更可能です。 そのため、これは値ではなく 参照渡しで渡してください。 値で渡すと、構造体のコピーが生成され、呼び出し元が状態の変更を確認できません。 ref 構造体の使用方法の詳細については、「安全で効率的な C# コードを記述する」をご覧ください。

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

プロパティ名の検索に ValueTextEquals を使用する

プロパティ名の検索用に 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();
}

関連項目