رضوى العربي

السلسلة إلى JSON

استخدام Json.NET

Newtonsoft.Json هي حزمة قوية، وسريعة، وسهلة الاستخدام مما جعلها الأداة الأكثر شيوعًا عند التعامل مع "السَلاسِل النصية بصيغة تبادل البيانات (JSON)" بإطار عمل ‎.NET. يُعدّ اِستخدامها سهلًا نوعًا ما؛ لكونها تعتمد على التوابع الساكنة (static) سواء للسَلسَلة (serialize)، أو لإلغاء السَلسَلة (de-serialize).

بالتحديد يُسَلسِل التابع JsonConvert.SerializeObject الكائن المُمرَّر له ويُعيد سِلسِلة نصية، بينما يُلغي التابع JsonConvert.DeserializeObject<T>‎ سَلسَلة المُعامِل المُمرَّر له ويُحاول تحليله إلى نوع يُحدَّد من خلال مُعامِل النوع (type parameter) كالمثالين التاليين:

using Newtonsoft.Json;

var rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}";

var fibo = JsonConvert.DeserializeObject<Dictionary<string, object>>(rawJSON);

var rawJSON2 = JsonConvert.SerializeObject(fibo);
internal class Sequence{
    public string Name;
    public List<int> Numbers;
}

string rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}";

Sequence sequence = JsonConvert.DeserializeObject<Sequence>(rawJSON);

اطلع على مزيد من المعلومات عن JSON.NET من خلال موقعها الرسمي.

يُدعِّم اطار عمل ‎.NET النوع JSON.NET منذ إصدار 2.

استخدام السمات (attributes) مع Json.NET

تتوفَّر بعض السمات (attributes) لزخرفة (decoration) كلًا من الأصناف وخاصياتها؛ مما يُمكنك من التحكم بشكل "السِلسِلة النصية بصيغة JSON" النهائية الناتجة عن السَلسَلة.

مثلًا، تُخصِص السمة JsonProperty اسمًا للخاصية المُزخَرفة بدلًا من اسمها الأصلي، في حين تُهمَل الخاصيات المزخرفة بالسمة JsonIgnore.

في المثال التالي: لا يحتوي ناتج السَلسَلة {"name":"Andrius","age":99} على الخاصية Address؛ لأنها مُزخرفة بالسمة JsonIgnore، كما اُستُخدِمت الأسماء name و age بدلًا من الأسماء الأصلية للخاصيات لأنها زُخرِفت بالسمة JsonProperty.

[JsonObject("person")]
public class Person
{
    [JsonProperty("name")]
    public string PersonName { get; set; }
    [JsonProperty("age")]
    public int PersonAge { get; set; }
    [JsonIgnore]
    public string Address { get; set; }
}

Person person = new Person { 
    PersonName = "Andrius", 
    PersonAge = 99, 
    Address = "Some address" 
};

string rawJson = JsonConvert.SerializeObject(person);
Console.WriteLine(rawJson); // {"name":"Andrius","age":99}

اطلع على مزيد من المعلومات عن سمات السَلسَلة التي يُمكنك استخدامها.

استخدام المُعامِل JsonSerializerSettings مع Json.NET

تتوفَّر بصمة أُخْرَى من التابع JsonConvert.SerializeObject والتي تَستقبِل -بالإضافة إلى الكائن المراد سَلسَلته- مُعامِلًا آخَر من النوع JsonSerializerSettings للتحكم بعملية السَلسَلة. يحتوي النوع JsonSerializerSettings على العديد من الخاصيات المُصممة خصيصًا لحل بعض أكثر المشاكل شيوعًا.

مثلًا، الخاصية ContractResolver من نوع الواجهة IContractResolver، والتي يتوفَّر لها أكثر من مُنفِّذ (implementation). إحداها هو النوع DefaultContractResolver المُستخدَم افتراضيًا، بالإضافة إلى النوع CamelCasePropertyNamesContractResolver الذي قد يُفيدك عند التحويل من كائنات c#‎ إلى صيغة JSON والعكس؛ نظرًا لشيوع استخدَام النمط PascalCase لدى مبرمجي c#‎ بينما في الغالب ما تكون صيغة تبادل البيانات JSON بنمط سنام الجمل CamelCase.

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    [JsonIgnore]
    public string Address { get; set; }
}

public void ToJson() {
    Person person = new Person { 
        Name = "Andrius", 
        Age = 99, 
        Address = "Some address" 
    };

    var resolver = new CamelCasePropertyNamesContractResolver();

    var settings = new JsonSerializerSettings { ContractResolver = resolver };

    string json = JsonConvert.SerializeObject(person, settings);
    Console.WriteLine(json); // {"name":"Andrius","age":99}
}

مثال آخر هو الخاصية NullValueHandling المُستخدَمة لضَبط طريقة التعامُل مع القيم الفارغة null، كالمثال التالي:

public static string Serialize(T obj)
{
    string result = JsonConvert.SerializeObject(obj, new JsonSerializerSettings { NullValueHandling
        = NullValueHandling.Ignore});
    return result;
}

مثال أخير هو الخاصية ReferenceLoopHandling التي تُستخدَم لضَبط طريقة التعامل مع تكرار الإشارة الذاتية (self referencing loop). مثلًا، إذا أردت تَمثيل برنامج دراسي مُلحق به عدد من الطلبة، سيكون لديك النوع Student يحوي خاصية من النوع Course والذي بدوره يحمل خاصية Students من النوع List<Student>‎.

public static string Serialize(T obj)
{
    string result = JsonConvert.SerializeObject(
        obj, 
        new JsonSerializerSettings {
            ReferenceLoopHandling = 
                ReferenceLoopHandling.Ignore});
    return result;
}

يُمكنك تخصيص أكثر من خاصية سَلسَلة كالتالي:

public static string Serialize(T obj)
{
    string result = JsonConvert.SerializeObject(
        obj, 
        new JsonSerializerSettings { 
            NullValueHandling = NullValueHandling.Ignore,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        });
    return result;
}

الربط الديناميكي (Dynamic Binding)

تُوفِّر Json.NET خاصية الربط الديناميكي، مما يَسمَح بإلغاء سَلسَلة "سِلسِلة نصية بصيغة JSON" وتحويلها إلى نوع ديناميكي لم يُعلَّن عن خاصياته صراحةً. يتم ذلك إِما باستخدام النوع DynamicObject أو النوع ExpandoObject.

السَلسَلة:

dynamic jsonObject = new ExpandoObject();
jsonObject.Title = "Merchent of Venice";
jsonObject.Author = "William Shakespeare";

Console.WriteLine(JsonConvert.SerializeObject(jsonObject));

في المثال التالي، حُُوِّلَت المفاتيح (keys) الموجودة "بالسِلسِلة النصية بصيغة JSON" إلى مُتَغيّرات أعضاء (member variables) بالنوع الديناميكي:

var rawJson = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}";
dynamic parsedJson = JObject.Parse(rawJson);

Console.WriteLine("Name: " + parsedJson.Name);
Console.WriteLine("Name: " + parsedJson.Numbers.Length);

قد يكون الربط الديناميكي مفيدًا خاصةً إذا كان البرنامج يَستقبِل ويُنتج العديد من "السَلاسِل النصية بصيغة JSON"، ولكن من الأفضل التحقق (validate) من تلك السَلاسِل النصية الناتجة عن السَلسَلة وكذلك من الكائنات الديناميكية الناتجة عن إلغائها.

استخدام JavascriptSerializer

يتوفَّر أيضًا النوع JavascriptSerializer -الموجود بفضاء الاسم System.Web.Script.Serialization- والذي يُعرِّف التابع Deserialize<T>(input)‎. يُلِّغي هذا التابع سَلسَلة "سِلسِلة نصية بصيغة تبادل البيانات (JSON)"، ويُحَوِّلها لكائن نوعه مُحدَّد بمُعامِل النوع (type parameter).

using System.Collections;
using System.Web.Script.Serialization;

string rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}";

JavaScriptSerializer JSS = new JavaScriptSerializer();

Dictionary<string, object> parsedObj = JSS.Deserialize<Dictionary<string, object>>(rawJSON);

string name = parsedObj["Name"].toString();
ArrayList numbers = (ArrayList)parsedObj["Numbers"]

يُدعِّم اطار عمل ‎.NET النوع JavaScriptSerializer منذ إصدار 3.5.

السلسلة إلى XML

استخدام XmlSerializer

يُستخدَم النوع XmlSerializer -الموجود بفضاء الاسم System.Xml.Serialization - لسَلسَلة كائن إلى ملف (document) نصي بصيغة لغة الترميز القابلة للامتداد Extensible Markup Language - XML.

public void SerializeFoo(string fileName, Foo foo)
{
    var serializer = new XmlSerializer(typeof(Foo));
    using (var stream = File.Open(fileName, FileMode.Create))
    {
        serializer.Serialize(stream, foo);
    }
}

يُمكن إلغاء السَلسَلة كالتالي:

public Foo DeserializeFoo(string fileName)
{
    var serializer = new XmlSerializer(typeof(Foo));
    using (var stream = File.OpenRead(fileName))
    {
        return (Foo)serializer.Deserialize(stream);
    }
}

استخدام السمات مع XmlSerializer

تتوفَّر بعض السمات (attributes) لزخرفة (decoration) خاصيات الأنواع؛ مما يُمكنك من التحكم بالصورة النهائية لملف XML الناتج عن السَلسَلة.

مثلًا، تُخصِص السمة XmlArray اسمًا للخاصيات من النوع Array -أو أي نوع قد يُعيد Array- بدلًا من اسمها الأصلي. في المثال التالي، اُستخدِم الاسم Articles بدلًا من الاسم Products نظرًا لأن الخاصية المُناظِرة زُخرِفت بالسمة XmlArray.

public class Store
{
    [XmlArray("Articles")]
    public List<Product> Products {get; set; }
}
<Store>
    <Articles>
        <Product/>
        <Product/>
    </Articles>
</Store>

في المثال التالي، تُخصِص السمة XmlElement اسمًا للخاصية المُزخَرفة بدلًا من اسمها الأصلي.

public class Foo
{
    [XmlElement(ElementName="Dog")]
    public Animal Cat { get; set; }
}
<Foo>
    <Dog/>
</Foo>

في المثال التالي، اُستخدِم جَالْب الخاصية لتحديد صيغة مُخصَّصة للقيمة العائدة بدلًا من الافتراضية.

public class Dog
{
    private const string _birthStringFormat = "yyyy-MM-dd";

    [XmlIgnore]
    public DateTime Birth {get; set;}

    [XmlElement(ElementName="Birth")]
    public string BirthString
    {
        get { return Birth.ToString(_birthStringFormat); }
        set { Birth = DateTime.ParseExact(value, _birthStringFormat, CultureInfo.InvariantCulture);
            }
    }
}

أخيرًا، تُهمَل الخاصيات المُزخَرفة بالسمة XmlIgnore.

سلسلة أصناف فرعية بشكل ديناميكي

المشكلة:

أحيانًا قد لا نستطيع استخدَام السِمات (attributes) لإمداد اطار عمل XmlSerializer بكل البيانات الوصفية (metadata) التي يحتاجها لإجراء السَلسَلة.

مثلًا، بفرض أن لدينا صنف أساسي (base class) مطلوب سَلسَلة كائناته. على الرغم من تَوفّر بعض السمات التي قد تُستخدَم للإشارة إلى أنواعه الفرعية، أحيانًا، قد لا تكون جميع الأصناف الفرعية مَعلومَة أثناء تصميم الصنف الأساسي؛ فمن المُحتمل وجود فريق آخر يَعمَل على تطوير بَعضًا من تلك الأنواع الفرعية. كما أنها حتى وإن تَوفَّرت فمِن المُفترض ألا يعلم الصنف الأساسي شيئًا عن أصنافه الفرعية.

حَل مُقْترَح:

نحتاج لإمداد المُسَلسِلات (serializers) بالأنواع المَعلومَة بطريقة أُخْرى، مثلًا، باستخدَام بَصمَة أُخْرى من باني الكائنات XmlSerializer(type, knownTypes)‎. تَستقبِل هذه البَصمة مصفوفة تحتوي على الأنواع المَعلومَة كمُعامِل ثان. قد تَفي هذه الطريقة بالغرض ولكنها بتعقيد زمني O(N^2)‎ على الأقل -إذا كان لدينا عدد N من المُسَلسِلات-، فقط لاكتشاف جميع الأنواع المُمرَّرة للمُعامِل.

var allSerializers = allTypes.Select(t => new XmlSerializer(t, allTypes));

var serializerDictionary = Enumerable.Range(0, allTypes.Length)
    .ToDictionary(i => allTypes[i], i => allSerializers[i]);

لاحظ أنه في المثال بالأعلى، لا علم للصنف الأساسي بالأصناف المشتقة منه، وهو ما يُعدّ أمرًا عاديًا -بل ومطلوبًا- بالبرمجة كائنية التوجه OOP.

حل أكثر كفاءة:

هناك لحسن الحظ طريقة تَعني بحل هذه المشكلة حلًا أكثر كفاءة عن طريق تَوفير مصفوفة بالأصناف المَعلومَة لعِدة مُسَلسِلات (serializers). تَستخدِم هذه الطريقة التابع FromTypes(Type[])، والذي يَسمَح بإنشاء مصفوفة من المُسَلسِلات من النوع XmlSerializer لمُعالجة مصفوفة من الأنواع (Type objects) بكفاءة.

var allSerializers = XmlSerializer.FromTypes(allTypes);

var serializerDictionary = Enumerable.Range(0, allTypes.Length)
    .ToDictionary(i => allTypes[i], i => allSerializers[i]);

بفرض وجود الأصناف التالية:

public class Container
{
    public Base Base { get; set; }
}

public class Base
{
    public int JustSomePropInBase { get; set; }
}

public class Derived : Base
{
    public int JustSomePropInDerived { get; set; }
}

نُنشئ كائن من النوع Container:

var sampleObject = new Container { Base = new Derived() };

نحاول أولًا سَلسَلته دون إمداد المُسَلسِل (serializer) بمعلومات عن النوع Derived:

var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) };
SetupSerializers(allTypes.Except(new[] { typeof(Derived) }).ToArray());
Serialize(sampleObject);

سيُنتَج عن ذلك رسالة خطأ. نحاول مُجددًا سَلسَلته مع إمداد المُسَلسِل بجميع الأنواع:

var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) };
SetupSerializers(allTypes);
Serialize(sampleObject);

ستتم العملية بنجاح.

الشيفرة بالكامل:

using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.Linq;
using System.Linq;

public class Program
{
    public class Container
    {
        public Base Base { get; set; }
    }

    public class Base
    {
        public int JustSomePropInBase { get; set; }
    }

    public class Derived : Base
    {
        public int JustSomePropInDerived { get; set; }
    }

    public void Main()
    {
        var sampleObject = new Container { Base = new Derived() };
        var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) };

        Console.WriteLine("Trying to serialize without a derived class metadata:");
        SetupSerializers(allTypes.Except(new[] { typeof(Derived) }).ToArray());
        try
        {
            Serialize(sampleObject);
        }
        catch (InvalidOperationException e)
        {
            Console.WriteLine();
            Console.WriteLine("This error was anticipated,");
            Console.WriteLine("we have not supplied a derived class.");
            Console.WriteLine(e);
        }
        Console.WriteLine("Now trying to serialize with all of the type information:");
        SetupSerializers(allTypes);
        Serialize(sampleObject);
        Console.WriteLine();
        Console.WriteLine("Slides down well this time!");
    }

    static void Serialize<T>(T o)
    {
        serializerDictionary[typeof(T)].Serialize(Console.Out, o);
    }

    private static Dictionary<Type, XmlSerializer> serializerDictionary;

    static void SetupSerializers(Type[] allTypes)
    {
        var allSerializers = XmlSerializer.FromTypes(allTypes);
        serializerDictionary = Enumerable.Range(0, allTypes.Length)
            .ToDictionary(i => allTypes[i], i => allSerializers[i]);
    }
}

الخْرج:

Trying to serialize without a derived class metadata:
<?xml version="1.0" encoding="utf-16"?>
<Container xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
This error was anticipated,
we have not supplied a derived class.
System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type Program+Derived was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write2_Base(String n, String ns, Base o, Boolean isNullable, Boolean needType)
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write3_Container(String n, String ns, Container o, Boolean isNullable, Boolean needType)
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write4_Container(Object o)
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces)
   at Program.Serialize[T](T o)
   at Program.Main()
Now trying to serialize with all of the type information:
<?xml version="1.0" encoding="utf-16"?>
<Container xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Base xsi:type="Derived">
    <JustSomePropInBase>0</JustSomePropInBase>
    <JustSomePropInDerived>0</JustSomePropInDerived>
  </Base>
</Container>
Slides down well this time!

تقترح رسالة الخطأ الآتي:

"اِستخدِم السمة XmlInclude أو السمة SoapInclude لتخصيص الأنواع غير المَعلومَة بشكل ثابت (statically)"

في الواقع، لا تستطيع دائمًا القيام بذلك -كما أشرنا من قبل-، وحتى إن كان ذلك مُمكنا، لا يُنصح بالقيام بذلك؛ لأن ليس من المُفترَض أن يعلم الصنف الأساسي شيئًا عن الأصناف المُشتقة منه فضلًا عن الإشارة إليها.

هذه هي الطريقة التي يَظهر بها النوع المُشتق بملف XML:

<Base xsi:type="Derived">

في المثال بالأعلى، تُشير كلمة Base إلى النوع المُصرَّح عنه للخاصية داخل الصنف Container، بينما كلمة Derived فهي تُشير إلى نوع النسخة المُسْنَدة للخاصية. (إليك مثال حي)

ترجمة -وبتصرف- للفصل Serialization من كتاب ‎.NET Framework Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن