فصل ۱۱: JSON در .NET؛ Utf8JsonReader، Utf8JsonWriter، JsonDocument و JsonNode
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
Utf8JsonReader
فایل JSON زیر را با نام people.json در نظر بگیرید:
{
"FirstName":"Sara",
"LastName":"Wells",
"Age":35,
"Friends":["Dylan","Ian"]
}
آکولادها یک Object در JSON را نشان میدهند که Propertyهایی مانند FirstName و LastName را در خود دارد؛ براکتها نیز یک Array در JSON را نشان میدهند که شامل Elementهای تکرارشونده است. در این مثال Elementهای تکرارشونده String هستند، اما میتوانستند Object یا Arrayهای دیگری باشند.
کد زیر فایل را با Enumerate کردن Tokenهای JSON Parse میکند. Token میتواند آغاز یا پایان یک Object، آغاز یا پایان یک Array، نام یک Property، یا مقدار یک Array/Property باشد؛ مقدار نیز میتواند String، Number، true، false یا null باشد:
byte[] data = File.ReadAllBytes ("people.json");
Utf8JsonReader reader = new Utf8JsonReader (data);
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
Console.WriteLine ($"Start of object");
break;
case JsonTokenType.EndObject:
Console.WriteLine ($"End of object");
break;
case JsonTokenType.StartArray:
Console.WriteLine();
Console.WriteLine ($"Start of array");
break;
case JsonTokenType.EndArray:
Console.WriteLine ($"End of array");
break;
case JsonTokenType.PropertyName:
Console.Write ($"Property: {reader.GetString()}");
break;
case JsonTokenType.String:
Console.WriteLine ($" Value: {reader.GetString()}");
break;
case JsonTokenType.Number:
Console.WriteLine ($" Value: {reader.GetInt32()}");
break;
default:
Console.WriteLine ($"No support for {reader.TokenType}");
break;
}
}
خروجی چنین است:
Start of object
Property: FirstName Value: Sara
Property: LastName Value: Wells
Property: Age Value: 35
Property: Friends
Start of array
Value: Dylan
Value: Ian
End of array
End of object
چون Utf8JsonReader مستقیماً با UTF-8 کار میکند، بدون اینکه ابتدا ورودی را به UTF-16 ــ قالب Stringهای .NET ــ تبدیل کند، Tokenها را یکییکی طی میکند. تبدیل به UTF-16 فقط زمانی رخ میدهد که متدی مانند GetString() را فراخوانی کنید.
نکتهٔ جالب این است که Constructor مربوط به Utf8JsonReader یک Byte Array نمیپذیرد، بلکه ReadOnlySpan<byte> میگیرد؛ به همین دلیل خود Utf8JsonReader بهصورت ref struct تعریف شده است. بااینحال میتوانید Byte Array را به آن بدهید، چون تبدیل ضمنی از T[] به ReadOnlySpan<T> وجود دارد. فصل ۲۳ توضیح میدهد Spanها چگونه کار میکنند و چگونه با کاهش Allocation حافظه به بهبود Performance کمک میکنند.
JsonReaderOptions
بهطور پیشفرض Utf8JsonReader میخواهد JSON کاملاً با استاندارد JSON RFC 8259 سازگار باشد. با فرستادن یک Instance از JsonReaderOptions به Constructor میتوانید Reader را تحملپذیرتر کنید. Optionها موارد زیر را کنترل میکنند:
- C-Style comments
- بهطور پیشفرض Comment در JSON باعث پرتاب
JsonException میشود. اگر CommentHandling را روی JsonCommentHandling.Skip بگذارید Commentها نادیده گرفته میشوند؛ مقدار JsonCommentHandling.Allow باعث میشود Reader آنها را بشناسد و هنگام برخورد، Token از نوع JsonTokenType.Comment تولید کند. Comment نمیتواند در میانهٔ Token دیگری قرار گیرد.
- Trailing commas
- طبق استاندارد، آخرین Property یک Object و آخرین Element یک Array نباید Comma انتهایی داشته باشد. فعالکردن
AllowTrailingCommas این محدودیت را آسانتر میکند.
- کنترل حداکثر عمق تودرتویی
- بهطور پیشفرض Objectها و Arrayها تا ۶۴ سطح میتوانند تودرتو شوند. با تنظیم
MaxDepth روی عددی دیگر، این مقدار تغییر میکند.
Utf8JsonWriter
System.Text.Json.Utf8JsonWriter یک Writer روبهجلو برای JSON است و از Typeهای زیر پشتیبانی میکند:
String و DateTime که بهصورت JSON String قالببندی میشوند؛- Typeهای عددی
Int32، UInt32، Int64، UInt64، Single، Double و Decimal که بهصورت JSON Number نوشته میشوند؛ bool که به Literalهای true/false تبدیل میشود؛- JSON
null؛ - Arrayها.
میتوانید این Typeها را طبق استاندارد JSON داخل Objectها سازماندهی کنید. Writer امکان نوشتن Comment را هم میدهد؛ Comment بخشی از استاندارد JSON نیست، ولی بسیاری از Parserها عملاً آن را پشتیبانی میکنند.
کد زیر نحوهٔ استفاده را نشان میدهد:
var options = new JsonWriterOptions { Indented = true };
using (var stream = File.Create ("MyFile.json"))
using (var writer = new Utf8JsonWriter (stream, options))
{
writer.WriteStartObject();
// Property name and value specified in one call
writer.WriteString ("FirstName", "Dylan");
writer.WriteString ("LastName", "Lockwood");
// Property name and value specified in separate calls
writer.WritePropertyName ("Age");
writer.WriteNumberValue (46);
writer.WriteCommentValue ("This is a (non-standard) comment");
writer.WriteEndObject();
}
خروجی فایل چنین است:
{
"FirstName": "Dylan",
"LastName": "Lockwood",
"Age": 46
/*This is a (non-standard) comment*/
}
از .NET 6، متد WriteRawValue در Utf8JsonWriter وجود دارد تا یک String یا Byte Array را مستقیماً وارد Streamِ JSON کند. این قابلیت در حالتهای ویژه مفید است؛ برای نمونه زمانی که میخواهید یک عدد همیشه با نقطهٔ اعشار نوشته شود، مثلاً 1.0 بهجای 1.
در این مثال Property با نام Indented در JsonWriterOptions روی true تنظیم شد تا خوانایی بهتر شود. بدون آن، خروجی فشرده خواهد بود:
{"FirstName":"Dylan","LastName":"Lockwood","Age":46...}
JsonWriterOptions همچنین Property با نام Encoder برای کنترل Escape کردن Stringها و SkipValidation برای دورزدن بررسیهای Structural Validation دارد؛ گزینهٔ دوم اجازه میدهد حتی JSON نامعتبر تولید شود.
JsonDocument
System.Text.Json.JsonDocument دادهٔ JSON را به یک DOM فقطخواندنی Parse میکند که از Instanceهای JsonElement تشکیل شده و بر حسب نیاز ایجاد میشوند. برخلاف Utf8JsonReader، با JsonDocument میتوانید به Elementها بهصورت Random Access دسترسی پیدا کنید.
JsonDocument یکی از دو API مبتنی بر DOM برای JSON است؛ دیگری JsonNode است که در بخش بعد میآید. JsonNode در .NET 6 عمدتاً برای پاسخ به نیاز یک DOM قابلنوشتن معرفی شد، اما برای Scenarioهای فقطخواندنی هم مناسب است و Interface روانتری ارائه میکند. پشت آن یک DOM سنتی قرار دارد که برای JSON Value، Array و Object از Class استفاده میکند. در مقابل، JsonDocument بسیار سبک است و عملاً یک Class اصلی با نام JsonDocument و دو Struct سبک با نامهای JsonElement و JsonProperty دارد که دادهٔ زیرین را بر حسب نیاز Parse میکنند. تفاوت در شکل 11-1 نمایش داده شده است.
شکل 11-1 — APIهای DOM برای JSONJsonDocument دادهٔ زیرین را بر حسب نیاز Parse میکند؛ JsonNode یک DOM قابلخواندن و قابلنوشتن با Objectهای مستقل فراهم میکند.
متد Static با نام Parse یک JsonDocument را از Stream، String یا Memory Buffer ایجاد میکند:
using JsonDocument document = JsonDocument.Parse (jsonString);
...
هنگام فراخوانی Parse میتوانید بهصورت اختیاری یک JsonDocumentOptions بدهید تا نحوهٔ برخورد با Trailing Comma، Comment و حداکثر عمق تودرتویی کنترل شود؛ این Optionها مانند JsonReaderOptions عمل میکنند.
سپس از طریق Property با نام RootElement به DOM دسترسی دارید:
using JsonDocument document = JsonDocument.Parse ("123");
JsonElement root = document.RootElement;
Console.WriteLine (root.ValueKind); // Number
JsonElement میتواند یک JSON Value (String، Number، true/false یا null)، Array یا Object را نشان دهد؛ Property با نام ValueKind مشخص میکند کدام نوع است.
خواندن مقدارهای ساده
اگر Element یک JSON Value باشد، با GetString، GetInt32، GetBoolean و مانند آن مقدارش را میخوانید:
using JsonDocument document = JsonDocument.Parse ("123");
int number = document.RootElement.GetInt32();
JsonElement متدهایی برای Parse کردن JSON String به Typeهای رایج CLR مثل DateTime و حتی Base-64 Binary نیز دارد و نسخههای TryGet* مانع پرتاب Exception در صورت شکست Parse میشوند.
خواندن JSON Arrayها
اگر JsonElement یک Array باشد، EnumerateArray() همهٔ Subitemها را بهصورت JsonElement Enumerate میکند و GetArrayLength() تعداد Elementها را میدهد. Indexer هم برای گرفتن Element در موقعیت مشخص قابلاستفاده است.
using JsonDocument document = JsonDocument.Parse (@"[1, 2, 3, 4, 5]");
int length = document.RootElement.GetArrayLength(); // 5
int value = document.RootElement[3].GetInt32(); // 4
خواندن JSON Objectها
اگر Element یک Object باشد، EnumerateObject() نام و Value همهٔ Propertyها را Enumerate میکند. GetProperty(string propertyName) Property را با نام برمیگرداند و اگر موجود نباشد Exception میدهد. TryGetProperty(string propertyName, out JsonElement value) نسخهٔ بدون Exception برای بررسی وجود Property است.
using JsonDocument document = JsonDocument.Parse (@"{ ""Age"": 32}");
JsonElement root = document.RootElement;
int age = root.GetProperty ("Age").GetInt32();
برای «کشف» Property با نام Age:
JsonProperty ageProp = root.EnumerateObject().First();
string name = ageProp.Name; // Age
JsonElement value = ageProp.Value;
Console.WriteLine (value.ValueKind); // Number
Console.WriteLine (value.GetInt32()); // 32
JsonDocument و LINQ
JsonDocument بهخوبی با LINQ هماهنگ است. فایل JSON زیر را در نظر بگیرید:
[
{
"FirstName":"Sara",
"LastName":"Wells",
"Age":35,
"Friends":["Ian"]
},
{
"FirstName":"Ian",
"LastName":"Weems",
"Age":42,
"Friends":["Joe","Eric","Li"]
},
{
"FirstName":"Dylan",
"LastName":"Lockwood",
"Age":46,
"Friends":["Sara","Ian"]
میتوان با JsonDocument و LINQ آن را چنین Query کرد:
using var stream = File.OpenRead (jsonPath);
using JsonDocument document = JsonDocument.Parse (json);
var query =
from person in document.RootElement.EnumerateArray()
select new
{
FirstName = person.GetProperty ("FirstName").GetString(),
Age = person.GetProperty ("Age").GetInt32(),
Friends =
from friend in person.GetProperty ("Friends").EnumerateArray()
select friend.GetString()
};
چون Queryهای LINQ بهصورت Lazy ارزیابی میشوند، باید Query را پیش از خروج document از Scope و Dispose ضمنیِ JsonDocument توسط Statement با نام using Enumerate کنید.
ایجاد نسخهٔ بهروزشده با JSON Writer
با اینکه JsonDocument فقطخواندنی است، میتوانید محتوای یک JsonElement را با متد WriteTo به Utf8JsonWriter بفرستید. این راهی برای تولید نسخهٔ تغییرکردهٔ JSON فراهم میکند. در مثال زیر از JSON قبل یک فایل جدید میسازیم که فقط افرادی با دو Friend یا بیشتر را شامل میشود:
using var json = File.OpenRead (jsonPath);
using JsonDocument document = JsonDocument.Parse (json);
var options = new JsonWriterOptions { Indented = true };
using (var outputStream = File.Create ("NewFile.json"))
using (var writer = new Utf8JsonWriter (outputStream, options))
{
writer.WriteStartArray();
foreach (var person in document.RootElement.EnumerateArray())
{
int friendCount = person.GetProperty ("Friends").GetArrayLength();
if (friendCount >= 2)
person.WriteTo (writer);
}
}
اگر به توانایی Update کردن خود DOM نیاز دارید، JsonNode انتخاب بهتری است.
JsonNode
JsonNode در Namespace با نام System.Text.Json.Nodes از .NET 6 معرفی شد، عمدتاً برای پاسخ به نیاز یک DOM قابلنوشتن. بااینحال برای Scenarioهای فقطخواندنی هم مناسب است.
JsonNode Interface نسبتاً روانی ارائه میکند و پشت آن یک DOM سنتی قرار دارد که برای JSON Value، Array و Object از Class استفاده میکند. Class بودن آنها هزینهٔ Garbage Collection دارد، ولی در بیشتر Scenarioهای واقعی احتمالاً ناچیز است. JsonNode همچنان بسیار Optimize شده و زمانی که Nodeهای یکسان بارها خوانده میشوند حتی میتواند از JsonDocument سریعتر باشد، چون JsonNode با اینکه Lazy است نتیجهٔ Parse را Cache میکند.
متد Static با نام Parse از Stream، String، Memory Buffer یا Utf8JsonReader یک JsonNode میسازد:
JsonNode node = JsonNode.Parse (jsonString);
در Parse نیز میتوانید JsonDocumentOptions بدهید تا Trailing Comma، Comment و Maximum Nesting Depth کنترل شود. برخلاف JsonDocument، JsonNode به Dispose نیاز ندارد.
Parse یک Subtype از JsonNode برمیگرداند که یکی از JsonValue، JsonObject یا JsonArray است. برای حذف شلوغی Downcast، Helperهای AsValue()، AsObject() و AsArray() وجود دارند:
var node = JsonNode.Parse ("123"); // Parses to a JsonValue
int number = node.AsValue().GetValue<int>();
// Shortcut for ((JsonValue)node).GetValue<int>();
بااینحال معمولاً لازم نیست این متدها را صدا بزنید، چون Memberهای پراستفاده مستقیماً روی JsonNode هم در دسترساند:
var node = JsonNode.Parse ("123");
int number = node.GetValue<int>();
// Shortcut for node.AsValue().GetValue<int>();
خواندن مقدارهای ساده
با GetValue<T> میتوانید یک مقدار ساده را Extract یا Parse کنید. JsonNode برای آسانترشدن این کار Operatorهای Explicit Cast زبان C# را Overload کرده است:
var node = JsonNode.Parse ("123");
int number = (int) node;
این قابلیت برای Typeهای عددی استاندارد، char، bool، DateTime، DateTimeOffset و Guid (و Nullableهای آنها)، همچنین string کار میکند.
اگر مطمئن نیستید Parse موفق میشود، باید از الگوی زیر استفاده کنید:
if (node.AsValue().TryGetValue<int> (out var number))
Console.WriteLine (number);
از .NET 8، فراخوانی node.GetValueKind() مشخص میکند Node یک String، Number، Array، Object یا true/false است.
خواندن JSON Arrayها
JsonNodeای که JSON Array را نمایش میدهد از Type با نام JsonArray است. JsonArray، IList<JsonNode> را پیادهسازی میکند؛ بنابراین میتوانید مانند Array یا List آن را Enumerate و با Index به Elementها دسترسی پیدا کنید:
var node = JsonNode.Parse (@"[1, 2, 3, 4, 5]");
Console.WriteLine (node.AsArray().Count); // 5
foreach (JsonNode child in node.AsArray())
{ ... }
بهعنوان Shortcut، Indexer مستقیماً از JsonNode قابلاستفاده است:
Console.WriteLine ((int)node[0]); // 1
از .NET 8 میتوانید متد GetValues<T> را نیز صدا بزنید تا داده بهصورت IEnumerable<T> برگردد:
int[] values = node.AsArray().GetValues<int>().ToArray();
خواندن JSON Objectها
JsonNodeای که Object را نمایش میدهد از Type با نام JsonObject است. JsonObject، IDictionary<string,JsonNode> را پیادهسازی میکند؛ بنابراین میتوانید با Indexer به Member دسترسی پیدا کنید یا Key/Value Pairهای Dictionary را Enumerate کنید.
var node = JsonNode.Parse (@"{ ""Name"":""Alice"", ""Age"": 32}");
string name = (string) node ["Name"]; // Alice
int age = (int) node ["Age"]; // 32
برای «کشف» Propertyهای Name و Age میتوانید روی Key/Value Pairهای Dictionary Enumerate کنید:
foreach (KeyValuePair<string,JsonNode> keyValuePair in node.AsObject())
{
string propertyName = keyValuePair.Key; // "Name" (then "Age")
JsonNode value = keyValuePair.Value;
}
اگر نمیدانید یک Property تعریف شده یا نه، این Pattern هم کار میکند:
if (node.AsObject().TryGetPropertyValue ("Name", out JsonNode nameNode))
{ ... }
پیمایش Fluent و LINQ
با Indexerها میتوانید مستقیماً به عمق Hierarchy بروید. برای نمونه، در فایل JSON زیر:
[
{
"FirstName":"Sara",
"LastName":"Wells",
"Age":35,
"Friends":["Ian"]
},
{
"FirstName":"Ian",
"LastName":"Weems",
"Age":42,
"Friends":["Joe","Eric","Li"]
},
{
"FirstName":"Dylan",
"LastName":"Lockwood",
"Age":46,
"Friends":["Sara","Ian"]
}
]
Friend سومِ Person دوم چنین استخراج میشود:
string li = (string) node[1]["Friends"][2];
Query کردن این فایل با LINQ نیز ساده است:
JsonNode node = JsonNode.Parse (File.ReadAllText (jsonPath));
var query =
from person in node.AsArray()
select new
{
FirstName = (string) person ["FirstName"],
Age = (int) person ["Age"],
Friends =
from friend in person ["Friends"].AsArray()
select (string) friend
};
برخلاف JsonDocument، JsonNode Disposable نیست؛ بنابراین در Lazy Enumeration نگرانیِ Dispose شدن آن در میانهٔ کار وجود ندارد.
بهروزرسانی با JsonNode
JsonObject و JsonArray Mutable هستند و میتوانید محتوایشان را تغییر دهید. سادهترین راه برای Replace یا Add کردن Property در JsonObject استفاده از Indexer است. در مثال زیر Value مربوط به Color از Red به White تغییر میکند و Property جدیدی با نام Valid افزوده میشود:
var node = JsonNode.Parse ("{ \"Color\": \"Red\" }");
node ["Color"] = "White";
node ["Valid"] = true;
Console.WriteLine (node.ToJsonString()); // {"Color":"White","Valid":true}
خط دوم Shortcut این عبارت است:
node ["Color"] = JsonValue.Create ("White");
بهجای یک Value ساده میتوانید یک JsonArray یا JsonObject را به Property نسبت دهید. ساخت این Objectها در بخش بعدی نشان داده میشود.
برای حذف Property، ابتدا به JsonObject Cast کنید یا AsObject را صدا بزنید و سپس Remove را فراخوانی کنید:
node.AsObject().Remove ("Valid");
JsonObject متد Add را نیز دارد که اگر Property از قبل موجود باشد Exception پرتاب میکند.
JsonArray هم اجازه میدهد Itemها را با Indexer Replace کنید:
var node = JsonNode.Parse ("[1, 2, 3]");
node[0] = 10;
با AsArray متدهای Add، Insert، Remove و RemoveAt در دسترس قرار میگیرند:
var arrayNode = JsonNode.Parse ("[1, 2, 3]");
arrayNode.AsArray().RemoveAt(0);
arrayNode.AsArray().Add (4);
Console.WriteLine (arrayNode.ToJsonString()); // [2,3,4]
از .NET 8 میتوانید با ReplaceWith نیز یک JsonNode را Update کنید:
var node = JsonNode.Parse ("{ \"Color\": \"Red\" }");
var color = node["Color"];
color.ReplaceWith ("Blue");
ساخت برنامهای DOM با JsonNode
JsonArray و JsonObject Constructorهایی دارند که Object Initialization Syntax را پشتیبانی میکنند؛ بنابراین میتوانید کل DOM را در یک Expression بسازید:
var node = new JsonArray
{
new JsonObject {
["Name"] = "Tracy",
["Age"] = 30,
["Friends"] = new JsonArray ("Lisa", "Joe")
},
new JsonObject {
["Name"] = "Jordyn",
["Age"] = 25,
["Friends"] = new JsonArray ("Tracy", "Li")
}
};
نتیجه JSON زیر است:
[
{
"Name": "Tracy",
"Age": 30,
"Friends": ["Lisa", "Joe"]
},
{
"Name": "Jordyn",
"Age": 25,
"Friends": ["Tracy","Li"]
}
]