System.Text.Json で JSON ドキュメント、Utf8JsonReader、Utf8JsonWriter を使用する方法
この記事では、次を使用する方法について説明します。
- JSON ペイロード内のデータにランダム アクセスするための JSON ドキュメント オブジェクト モデル (DOM)。
- カスタム シリアライザーを構築するための
Utf8JsonWriter
型。 - カスタム パーサーと逆シリアライザーを構築するための
Utf8JsonReader
型。
JSON DOM の選択肢
DOM を使用することは、JsonSerializer による逆シリアル化の代替手段です。
- 逆シリアル化する型がない場合。
- 受信した JSON に固定スキーマがなく、含まれている内容を確認するために検査する必要がある場合。
System.Text.Json
には、JSON DOM を構築する 2 つの方法が用意されています。
- JsonDocument を使用すると、
Utf8JsonReader
を使用して読み取り専用 DOM を構築することができます。 ペイロードを構成する JSON 要素には、JsonElement 型を使用してアクセスできます。JsonElement
型では、配列とオブジェクト列挙子と共に、JSON テキストを一般的な .NET 型に変換する API が提供されます。JsonDocument
では RootElement プロパティが公開されます。 詳細については、この記事で後述する「JsonDocument の使用」を参照してください。
- System.Text.Json.Nodes 名前空間の JsonNode およびその派生クラスを使用すると、変更可能な DOM を作成することができます。 ペイロードを構成する JSON 要素には、JsonNode、JsonObject、JsonArray、JsonValue、JsonElement 型を使用してアクセスできます。 詳細については、この記事で後述する「
JsonNode
の使用」を参照してください。
JsonDocument
と JsonNode
のどちらを使用するか選ぶときは、次の要素を考慮してください。
JsonNode
DOM は作成後に変更できます。JsonDocument
DOM は変更できません。JsonDocument
DOM では、そのデータにより高速にアクセスできます。
- .NET 6 以降、System.Text.Json.Nodes 名前空間の JsonNode およびその派生クラスを使用すると、変更可能な DOM を作成することができます。 詳細については、この記事の .NET 6 バージョンを参照してください。
JsonNode
を使用します
以下の例では、System.Text.Json.Nodes 名前空間の JsonNode と他の型を使用して、次の操作を行う方法を示します。
- JSON 文字列から DOM を作成する
- DOM から JSON を書き込む。
- DOM から値、オブジェクト、または配列を取得する。
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.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.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.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 を割り当てます。
JsonArray
のCount
プロパティから学生の数を取得します。
JsonSerializerOptions
を含む JsonNode
JsonSerializer
を使用すると、JsonNode
のインスタンスをシリアル化したり、逆シリアル化したりすることができます。 ただし、JsonSerializerOptions
を取るオーバーロードを使用する場合、オプション インスタンスはカスタム コンバーターを取得するためにのみ使用されます。 オプション インスタンスのその他の機能は使用されていません。 たとえば、JsonSerializerOptions.DefaultIgnoreCondition を WhenWritingNull に設定し、JsonSerializerOptions
を取るオーバーロードを使用して JsonSerializer
を呼び出した場合、null のプロパティは無視されません。
同じ制限が、JsonSerializerOptions
パラメーターとして WriteTo(Utf8JsonWriter, JsonSerializerOptions) や ToJsonString(JsonSerializerOptions) を取る JsonNode
メソッドに適用されます。 これらの API では、カスタム コンバーターを取得するためにのみ JsonSerializerOptions
を使用します。
次の例は、JsonSerializerOptions
パラメーターを取り、JsonNode
インスタンスをシリアル化するメソッドを使用した結果を示しています。
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace JsonNodeWithJsonSerializerOptions;
public class Program
{
public static void Main()
{
Person person = new Person { Name = "Nancy" };
// Default serialization - Address property included with null token.
// Output: {"Name":"Nancy","Address":null}
string personJsonWithNull = JsonSerializer.Serialize(person);
Console.WriteLine(personJsonWithNull);
// Serialize and ignore null properties - null Address property is omitted
// Output: {"Name":"Nancy"}
JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
Console.WriteLine(personJsonWithoutNull);
// Ignore null properties doesn't work when serializing JsonNode instance
// by using JsonSerializer.
// Output: {"Name":"Nancy","Address":null}
var personJsonNode = JsonSerializer.Deserialize<JsonNode>(personJsonWithNull);
personJsonWithNull = JsonSerializer.Serialize(personJsonNode, options);
Console.WriteLine(personJsonWithNull);
// Ignore null properties doesn't work when serializing JsonNode instance
// by using JsonNode.ToJsonString method.
// Output: {"Name":"Nancy","Address":null}
personJsonWithNull = personJsonNode!.ToJsonString(options);
Console.WriteLine(personJsonWithNull);
// Ignore null properties doesn't work when serializing JsonNode instance
// by using JsonNode.WriteTo method.
// Output: {"Name":"Nancy","Address":null}
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
personJsonNode!.WriteTo(writer, options);
writer.Flush();
personJsonWithNull = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(personJsonWithNull);
}
}
public class Person
{
public string? Name { get; set; }
public string? Address { get; set; }
}
カスタム コンバーター以外の JsonSerializerOptions
の機能が必要な場合は、JsonNode
ではなく、厳密に型指定されたターゲット (この例の Person
クラスなど) を指定して JsonSerializer
を使用します。
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
オブジェクトで検索する場合は、次の方法を使用してパフォーマンスを最適化してください。
- 独自のインデックス作成やループを実行するのではなく、組み込みの列挙子 (EnumerateArray と EnumerateObject) を使用します。
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 ドキュメント内の特定の要素を操作する必要がある場合は、その JsonElement の Clone を返します。 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
が作成されます。 このメソッドでは、呼び出し元がドキュメント全体を操作することが想定されているため、RootElement
の Clone
が返されます。
JsonElement
を受け取り、サブ要素を返す場合は、サブ要素の Clone
を返す必要はありません。 呼び出し元は、渡された JsonElement
が属している JsonDocument
が破棄されないように維持する役割を負っています。 次に例を示します。
public JsonElement ReturnFileName(JsonElement source)
{
return source.GetProperty("fileName");
}
JsonSerializerOptions
を含む JsonDocument
JsonSerializer
を使用すると、JsonDocument
のインスタンスをシリアル化したり、逆シリアル化したりすることができます。 ただし、JsonSerializer
を使用した JsonDocument
インスタンスの読み取りと書き込みの実装は、JsonDocument.ParseValue(Utf8JsonReader) と JsonDocument.WriteTo(Utf8JsonWriter) に対するラッパーです。 このラッパーは、どの JsonSerializerOptions
(シリアライザー機能) も Utf8JsonReader
や Utf8JsonWriter
に転送しません。 たとえば、JsonSerializerOptions.DefaultIgnoreCondition を WhenWritingNull に設定し、JsonSerializerOptions
を取るオーバーロードを使用して JsonSerializer
を呼び出した場合、null のプロパティは無視されません。
次の例は、JsonSerializerOptions
パラメーターを取り、JsonDocument
インスタンスをシリアル化するメソッドを使用した結果を示しています。
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace JsonDocumentWithJsonSerializerOptions;
public class Program
{
public static void Main()
{
Person person = new Person { Name = "Nancy" };
// Default serialization - Address property included with null token.
// Output: {"Name":"Nancy","Address":null}
string personJsonWithNull = JsonSerializer.Serialize(person);
Console.WriteLine(personJsonWithNull);
// Serialize and ignore null properties - null Address property is omitted
// Output: {"Name":"Nancy"}
JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
Console.WriteLine(personJsonWithoutNull);
// Ignore null properties doesn't work when serializing JsonDocument instance
// by using JsonSerializer.
// Output: {"Name":"Nancy","Address":null}
var personJsonDocument = JsonSerializer.Deserialize<JsonDocument>(personJsonWithNull);
personJsonWithNull = JsonSerializer.Serialize(personJsonDocument, options);
Console.WriteLine(personJsonWithNull);
}
}
public class Person
{
public string? Name { get; set; }
public string? Address { get; set; }
}
JsonSerializerOptions
の機能が必要な場合は、JsonDocument
ではなく、厳密に型指定されたターゲット (この例の Person
クラスなど) を指定して JsonSerializer
を使用します。
Utf8JsonWriter
を使用します
Utf8JsonWriter は、String
、Int32
、DateTime
のような一般的な .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.Text; using System.Text.Json; 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 // } // ] //}
文字のエスケープ処理をカスタマイズする
JsonTextWriter
の StringEscapeHandling 設定には、すべての ASCII 以外の文字または HTML 文字をエスケープするオプションが用意されています。 既定では、Utf8JsonWriter
ではすべての ASCII 以外および HTML 文字がエスケープされます。 このエスケープ処理は、多層防御セキュリティ上の理由で行われます。 別のエスケープ処理ポリシーを指定するには、JavaScriptEncoder を作成し、JsonWriterOptions.Encoder を設定します。 詳細については、「文字エンコードをカスタマイズする」を参照してください。
null 値を書き込む
Utf8JsonWriter
を使用して null 値を書き込むには、以下を呼び出します。
- WriteNull。null を値として指定し、キーと値のペアを書き込みます。
- WriteNullValue。JSON 配列の要素として null を書き込みます。
文字列プロパティでは、文字列が null の場合、WriteString と WriteStringValue は WriteNull
と WriteNullValue
に相当します。
Timespan、Uri、または char の値を書き込む
Timespan
、Uri
、または 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.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"
}
]
デコードされた JSON 文字列を使用する
.NET 7 以降では、デコードされた JSON 文字列を使用する場合に、Utf8JsonReader.GetString() の代わりに Utf8JsonReader.CopyString メソッドを使用できます。 常に新しい文字列を割り当てる GetString() とは異なり、CopyString ではエスケープされていない文字列を自分が所有するバッファにコピーできます。 次のコード スニペットは、CopyString を使用して UTF-16 文字列を使用する例を示しています。
int valueLength = reader.HasReadOnlySequence
? checked((int)ValueSequence.Length)
: ValueSpan.Length;
char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.Slice(0, charsRead);
// Handle the unescaped JSON string.
ParseUnescapedString(source);
ArrayPool<char>.Shared.Return(buffer, clearArray: true);
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.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, buffer, 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, buffer, reader);
}
// Display value of Summary property, that is, "Hot".
Console.WriteLine($"Got property value: {reader.GetString()}");
}
private static void GetMoreBytesFromStream(
MemoryStream stream, byte[] buffer, 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();
}
関連項目
- System.Text.Json の概要
- JsonSerializerOptions インスタンスのインスタンスを作成する
- 大文字と小文字を区別しない一致を有効にする
- プロパティの名前と値をカスタマイズする
- プロパティを無視する
- 無効な JSON を許可する
- オーバーフロー JSON を処理するか、JsonElement または JsonNode を使用する
- 参照を保持し、循環参照を処理する
- 変更できない型と非パブリック アクセサーに逆シリアル化する
- ポリモーフィックなシリアル化
- Newtonsoft.Json から System.Text.Json に移行する
- 文字エンコードをカスタマイズする
- DOM、Utf8JsonReader、Utf8JsonWriter を使用する
- JSON シリアル化のためのカスタム コンバーターの作成
- DateTime および DateTimeOffset のサポート
- ソース生成を使用する方法
- サポートされているコレクション型
- System.Text.Json API リファレンス
- System.Text.Json.Serialization API リファレンス