Menu
flag Ar iconالعربيةdown icon

JSON في C#: التسلسل وإلغاء التسلسل بـ System.Text.Json

كيف تعمل مع JSON في C# باستخدام System.Text.Json: JsonSerializer.Serialize وDeserialize، ومخرجات camelCase والمخرجات المنسّقة، والسمات مثل JsonPropertyName وJsonIgnore، والتعدادات كنصوص، وقراءة JSON دون أصناف عبر JsonDocument وJsonNode، والأخطاء، ومقارنتها بـ Newtonsoft.Json.

JSON هو الطريقة التي تتحدث بها معظم برامج C# مع واجهات API على الويب، وتخزّن بها الإعدادات، وتتبادل البيانات. تقرؤه .NET الحديثة وتكتبه بـ System.Text.Json، وهي جزء من وقت التشغيل منذ .NET Core 3.0: لا حاجة إلى حزمة NuGet. ونقطة دخولها الرئيسية الصنف الساكن JsonSerializer، الذي يحوّل الكائنات إلى نصوص JSON والعكس.

System.Text.Json مدمجة في .NET Core 3.0 وفي كل إصدار بعده (من .NET 5 حتى .NET 10). ويمكن لمشاريع .NET Framework 4.6.2 وما بعده، أو .NET Standard 2.0، استخدامها أيضًا بتثبيت حزمة NuGet المسماة System.Text.Json. تُعرض أمثلة هذه الصفحة كشيفرة عادية مع المخرجات في التعليقات. أضف موجّهات using هذه لتشغيلها في مشروع .NET:

using System.Text.Json;
using System.Text.Json.Serialization;

التسلسل: من كائن إلى JSON

تكتب JsonSerializer.Serialize كل خاصية عامة في كائن، باستخدام أسماء الخصائص كما هي:

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public List<string> Tags { get; set; } = new List<string>();
    public bool InStock { get; set; }
}

var lamp = new Product { Name = "Desk lamp", Price = 34.90m, Tags = { "home", "light" }, InStock = true };

string json = JsonSerializer.Serialize(lamp);
Console.WriteLine(json);
// {"Name":"Desk lamp","Price":34.90,"Tags":["home","light"],"InStock":true}

تصبح المجموعات مصفوفات، والقواميس ذات المفاتيح النصية كائنات ({"apples":3,"pears":5})، وتبقى null هي null، ويصبح DateTime نصًا بصيغة ISO 8601 ("2026-03-01T14:30:00"). افتراضيان يوقعان الناس:

  • الحقول تُتخطّى. لا تُسلسل إلا الخصائص. يُسلسل صنف فيه public int X; كـ {} ما لم تضبط IncludeFields = true في الخيارات أو تحوّل الحقل إلى خاصية.
  • التعدادات أرقام. تُكتب OrderStatus.Shipped كـ 1 (القيمة الأساسية لـ التعداد). أضف JsonStringEnumConverter (أدناه) لتكتب "Shipped".

إلغاء التسلسل: من JSON إلى كائن

تنشئ JsonSerializer.Deserialize<T> كائن T وتضبط خصائصه من JSON:

string json = "{\"Name\":\"Desk lamp\",\"Price\":34.90,\"Tags\":[\"home\",\"light\"]}";
Product p = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{p.Name} {p.Price} {p.Tags.Count}");   // Desk lamp 34.90 2

var many = JsonSerializer.Deserialize<List<Product>>("[{\"Name\":\"A\",\"Price\":1},{\"Name\":\"B\",\"Price\":2.5}]");
Console.WriteLine(many.Count);                               // 2

تُتجاهل خصائص JSON التي لا خاصية C# مطابقة لها، وتحتفظ خصائص C# التي لا JSON مطابقًا لها بقيمها الافتراضية. ولا يُعدّ أي منهما خطأ افتراضيًا.

القاعدة التي توقع الجميع تقريبًا: مطابقة الأسماء حساسة لحالة الأحرف. معظم واجهات API على الويب ترسل camelCase، وcamelCase لا تطابق خصائص PascalCase:

string fromApi = "{\"name\":\"Mug\",\"price\":8.5}";

var a = JsonSerializer.Deserialize<Product>(fromApi);
Console.WriteLine($"[{a.Name}] {a.Price}");    // [] 0: nothing matched, no error

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var b = JsonSerializer.Deserialize<Product>(fromApi, options);
Console.WriteLine($"[{b.Name}] {b.Price}");    // [Mug] 8.5

تعطي new JsonSerializerOptions(JsonSerializerDefaults.Web) الإعدادات التي يستخدمها ASP.NET Core: قراءة غير حساسة لحالة الأحرف، وكتابة بـ camelCase، وقبول الأرقام المكتوبة كنصوص بين علامات اقتباس.

يحتاج إلغاء التسلسل إلى طريقة لضبط كل قيمة: ضابط عام، أو موصّل init، أو مُنشئ تطابق أسماء معاملاته الخصائص. وهذه القاعدة الأخيرة سبب عمل السجلات دون إعداد:

public record Point(int X, int Y);

Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt);   // Point { X = 1, Y = 2 }

الخيارات: camelCase والمخرجات المنسّقة

تتحكم JsonSerializerOptions في التسمية والتنسيق والمزيد. أنشئ مثيلًا واحدًا وأعد استخدامه: يخزّن المسلسِل البيانات الوصفية مؤقتًا لكل مثيل خيارات، فبناء خيارات جديدة لكل استدعاء أبطأ بشكل ملموس.

private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true,
};

Console.WriteLine(JsonSerializer.Serialize(lamp, Options));
// {
//   "name": "Desk lamp",
//   "price": 34.90,
//   "tags": [
//     "home",
//     "light"
//   ],
//   "inStock": true
// }

خيارات أخرى تستحق المعرفة: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull لحذف الخصائص null، وIncludeFields = true، وNumberHandling لقبول الأرقام المكتوبة كنصوص، وReadCommentHandling = JsonCommentHandling.Skip مع AllowTrailingCommas = true لملفات الإعدادات المحرّرة يدويًا. وأضافت .NET 8 السياسة JsonNamingPolicy.SnakeCaseLower لواجهات API التي تستخدم snake_case.

السمات: إعادة التسمية والتجاهل والتعدادات كنصوص

تتحكم السمات على الصنف في خاصية واحدة في كل مرة وتتغلب على الخيارات:

public enum OrderStatus { Pending, Shipped }

public class Order
{
    [JsonPropertyName("order_id")]
    public int Id { get; set; }

    public string Customer { get; set; }

    [JsonIgnore]
    public string InternalNote { get; set; }                  // never written or read

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Coupon { get; set; }                        // left out when null

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public OrderStatus Status { get; set; }

    public DateTime PlacedAt { get; set; }
}

var order = new Order
{
    Id = 1042, Customer = "Ana", InternalNote = "vip",
    Status = OrderStatus.Shipped, PlacedAt = new DateTime(2026, 3, 1, 14, 30, 0),
};
Console.WriteLine(JsonSerializer.Serialize(order));
// {"order_id":1042,"Customer":"Ana","Status":"Shipped","PlacedAt":"2026-03-01T14:30:00"}

ولكتابة كل تعداد كنص بدل تعليم كل خاصية، أضف المحوّل إلى الخيارات: options.Converters.Add(new JsonStringEnumConverter());.

قراءة JSON دون صنف: JsonDocument وJsonNode

حين لا تحتاج إلا إلى بضع قيم من استجابة كبيرة، أو يتغيّر شكلها، تخطَّ الصنف. تحلّل JsonDocument إلى شجرة للقراءة فقط من قيم JsonElement:

string weather = "{\"city\":\"Lisbon\",\"current\":{\"temp\":21.5,\"conditions\":[\"sunny\",\"windy\"]},\"alerts\":null}";

using (JsonDocument doc = JsonDocument.Parse(weather))
{
    JsonElement root = doc.RootElement;
    Console.WriteLine(root.GetProperty("city").GetString());                        // Lisbon
    Console.WriteLine(root.GetProperty("current").GetProperty("temp").GetDecimal()); // 21.5

    foreach (JsonElement c in root.GetProperty("current").GetProperty("conditions").EnumerateArray())
        Console.WriteLine(c.GetString());                                            // sunny, windy

    Console.WriteLine(root.TryGetProperty("humidity", out _));                       // False
    Console.WriteLine(root.GetProperty("alerts").ValueKind);                         // Null
}

ترمي GetProperty استثناء KeyNotFoundException لاسم مفقود، فاستخدم TryGetProperty للحقول الاختيارية. وتستأجر JsonDocument ذاكرة من مجمّع، ولهذا يُتخلّص منها بـ using.

ولتعديل JSON استخدم JsonNode من System.Text.Json.Nodes (.NET 6 وما بعده)، التي تعطي شجرة قابلة للتغيير بمفهرسات:

JsonNode node = JsonNode.Parse(weather);
Console.WriteLine((string)node["city"]);   // Lisbon

node["current"]["temp"] = 23;
node["updated"] = true;
Console.WriteLine(node.ToJsonString());
// {"city":"Lisbon","current":{"temp":23,"conditions":["sunny","windy"]},"alerts":null,"updated":true}

الملفات والتدفقات

JSON على القرص نص في ملف، فتتركّب دوال الملفات مباشرة مع المسلسِل:

File.WriteAllText("settings.json", JsonSerializer.Serialize(settings, Options));
var loaded = JsonSerializer.Deserialize<Settings>(File.ReadAllText("settings.json"), Options);
// Same options both ways: camelCase names written with Options would not match on a default read.

وللملفات الكبيرة وأجسام HTTP تتجنّب أشكال التدفق غير المتزامنة بناء النص كله في الذاكرة:

await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);

في ASP.NET Core ومع HttpClient نادرًا ما تستدعي المسلسِل بنفسك: تربط وحدات التحكم أجسام JSON تلقائيًا، وتنفّذ httpClient.GetFromJsonAsync<Order>(url) (في System.Net.Http.Json) الطلب وإلغاء التسلسل في استدعاء واحد.

الأخطاء

JSON غير الصالح، أو القيمة التي لا يمكن تحويلها إلى نوع الخاصية، يرمي JsonException. تذكر رسالته مسار JSON والموضع، وهذا يكفي عادة لإيجاد المشكلة:

try
{
    JsonSerializer.Deserialize<Product>("{\"Name\": \"Lamp\", \"Price\": \"cheap\"}");
}
catch (JsonException e)
{
    Console.WriteLine(e.Message);
    // The JSON value could not be converted to System.Decimal. Path: $.Price | LineNumber: 0 | BytePositionInLine: 33.
}

تعيد Deserialize القيمة null (لا استثناء) حين يكون نص JSON هو القيمة الحرفية null، فافحص النتيجة حين تأتي المدخلات من الخارج.

المحارف المهرّبة في المخرجات

افتراضيًا يهرّب المسلسِل المحارف غير ASCII والمحارف غير الآمنة داخل HTML:

Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}

هذا JSON صالح ويُقرأ بالنص الأصلي؛ يُهرَّب كي يمكن وضع المخرجات في صفحة HTML بأمان. للملفات المقروءة للإنسان اضبط Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (من System.Text.Encodings.Web) في الخيارات. وكلمة "unsafe" لا تشير إلا إلى تضمين النتيجة في HTML.

System.Text.Json مقابل Newtonsoft.Json

كانت Newtonsoft.Json (Json.NET، الصنف JsonConvert) المعيار لعقد وما زالت في كل مكان. الفروق الرئيسية:

System.Text.JsonNewtonsoft.Json
التوفّرمدمجة في .NET Core 3.0+، وحزمة NuGet لـ .NET Framework 4.6.2+حزمة NuGet لـ .NET Framework و.NET
التسلسل / إلغاؤهJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
مطابقة الأسماءحساسة لحالة الأحرف افتراضيًاغير حساسة لحالة الأحرف
القراءة دون أنواعJsonDocument، JsonNodeJObject، JToken، مع استعلامات JSONPath
إعادة تسمية خاصية[JsonPropertyName("x")][JsonProperty("x")]
التساهلصارمة: لا تعليقات ولا فواصل زائدة ولا أرقام بين علامات اقتباس ما لم تُفعَّلمتساهلة افتراضيًا
الأداءأسرع، حجوزات أقل، توليد المصدر للتقليم وAOTأبطأ، قائمة على الانعكاس

للسمات أسماء وفضاءات أسماء مختلفة، فتبديل قاعدة شيفرة عمل بحث واستبدال إضافة إلى اختبارات للقراءة الأكثر صرامة. للشيفرة الجديدة في .NET ابدأ بـ System.Text.Json.

توليد المصدر (.NET 6+)

يفحص JsonSerializer عادة أنواعك بالانعكاس وقت التشغيل. أما للتطبيقات المقلّمة أو المترجمة مسبقًا (Native AOT وBlazor WebAssembly) فيكتب مولّد مصدر تلك الشيفرة وقت الترجمة بدلًا من ذلك:

[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }

string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);

أخطاء شائعة

  • JSON بنمط camelCase إلى خصائص PascalCase بالخيارات الافتراضية. تبقى كل خاصية فارغة، بصمت. استخدم PropertyNameCaseInsensitive أو JsonSerializerDefaults.Web.
  • حقول عامة بدل الخصائص. لا تُسلسل ما لم يُضبط IncludeFields.
  • خصائص بلا ضابط. الخاصية للقراءة فقط دون معامل مُنشئ مطابق لا تُملأ عند إلغاء التسلسل.
  • JsonSerializerOptions جديدة لكل استدعاء. أعد استخدام مثيل ساكن واحد.
  • المال بالفاصلة العائمة. يُسلسل double قيمته 0.1 + 0.2 كـ 0.30000000000000004. استخدم decimal للمبالغ.

الأسئلة الشائعة

كيف أحوّل كائنًا إلى JSON في C#؟

استدعِ JsonSerializer.Serialize(obj) من System.Text.Json، المدمجة في .NET Core 3.0 وما بعده دون حزمة للتثبيت. تكتب كل خاصية عامة: {"Name":"Desk lamp","Price":34.90}. مرّر new JsonSerializerOptions { WriteIndented = true } لمخرجات مقروءة وPropertyNamingPolicy = JsonNamingPolicy.CamelCase لأسماء بنمط camelCase.

كيف أحوّل JSON إلى كائن في C#؟

تنشئ var product = JsonSerializer.Deserialize<Product>(json); كائن Product وتملأ خصائصه العامة القابلة للضبط من أسماء JSON المطابقة. المطابقة حساسة لحالة الأحرف افتراضيًا، فيترك JSON بنمط camelCase الخصائص بنمط PascalCase فارغة ما لم تمرّر PropertyNameCaseInsensitive = true أو new JsonSerializerOptions(JsonSerializerDefaults.Web). وJSON المشوّه أو نوع القيمة الخاطئ يرمي JsonException.

كيف أقرأ JSON دون إنشاء صنف في C#؟

استخدم JsonDocument.Parse(json) وامشِ على RootElement بـ GetProperty("name") وGetString() وGetInt32() وEnumerateArray()؛ هي للقراءة فقط وسريعة، ويجب التخلّص منها. وللـ JSON الذي تريد تعديله تعطيك JsonNode.Parse(json) (.NET 6+) شجرة قابلة للتغيير: node["city"]، والإسنادات، وToJsonString().

هل أستخدم System.Text.Json أم Newtonsoft.Json؟

للشيفرة الجديدة على .NET Core 3.0 أو أحدث، System.Text.Json: مدمجة، وأسرع، وتحجز أقل، ويستخدمها ASP.NET Core افتراضيًا. أما Newtonsoft.Json (Json.NET) فما زالت شائعة في مشاريع .NET Framework (حيث لا تتوفر System.Text.Json إلا كحزمة NuGet)، وفي الشيفرة التي تعتمد على ميزاتها الإضافية (استعلامات JSONPath، والقراءة المتساهلة جدًا، وTypeNameHandling)، وفي قواعد الشيفرة الكبيرة المبنية عليها أصلًا.

لماذا تهرّب System.Text.Json محارف مثل é و<؟

يهرّب المرمّز الافتراضي المحارف غير ASCII والمحارف الحساسة في HTML (< و> و& و') بالشكل \uXXXX، كي تكون المخرجات آمنة للتضمين في HTML. وما زالت JSON صالحة وتعود عند إلغاء التسلسل إلى النص نفسه. للمخرجات المقروءة اضبط Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping في الخيارات، لكن فقط حين لا يُكتب JSON داخل HTML.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن