利用 DataContractJsonSerializer 實現物件序列化與反序列化

DataContractJsonSerializer 類別是 .NET Framework 3.5 的新成員,它可以用來將物件序列化成 JSON 格式的字串,或者執行反序列化,也就是根據 JSON 格式的字串內容來建立物件,並還原物件的狀態。

DataContractJsonSerializer 的用法很簡單,網路上已經可以查到不少範例和教學文章,例如 Chris Pietschmann 的 .NET 3.5: JSON Serialization using the DataContractJsonSerializer

Pietschmann 的文章裡有提供一個 JsonHelper 類別,在撰寫序列化/反序列化程式時挺方便的。我做了點小修改,把裡面的 Encoding.Default 改為 Encoding.Utf8,然後寫個小程式測試一下,以確認 DataContractJsonSerializer 是否符合我的需求,主要是:
  • 支援深層序列化(巢狀屬性)
  • 新舊版本的容錯(稍後說明)
  • 支援自訂型別和常用的 .NET 型別,包括:string、int、double、DateTime、bool 等。
為了測試至少三層的巢狀序列化,我寫了幾個簡單的類別,包括 Order(訂單)、OrderItem(訂單細項)、Product(產品),程式碼如下:
[DataContract]
public class Order
{
    private List<OrderItem> items;

    public Order()
    {
    }

    public Order(int id, DateTime orderDate, bool useCoupon)
    {
        this.ID = id;
        this.OrderDate = orderDate;
        this.UseCoupon = useCoupon;
        this.items = new List<OrderItem>();
    }

    [DataMember]
    public int ID { get; set; }

    [DataMember]
    public DateTime OrderDate { get; set; }

    [DataMember]
    public bool UseCoupon { get; set; }

    [DataMember]
    public List<OrderItem> Items
    {
        get { return items; }
        set { items = value; }
    }
}

[DataContract]
public class OrderItem 
{
    public OrderItem(Product product, int count)
    {
        this.Product = product;
        this.Count = count;
    }

    [DataMember]
    public Product Product { get; set; }

    [DataMember]
    public int Count { get; set; }
}

[DataContract]
public class Product
{
    public Product(string name, double price)
    {
        this.Name = name;
        this.Price = price;
    }

    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public double Price { get; set; }
}

要點:欲序列化的類別需套用 DataContract attribute;同時,欲序列化的類別屬性還必須套用 DataMember attribute,否則該屬性不會參與序列化/反序列化過程。


接著寫一個 Windows Forms 程式來觀察序列化和反序列化的結果:在 form 上面放兩個 Button 和一個 TextBox,兩個按鈕分別執行序列化和反序列化的動作,結果均輸出至 TextBox。程式碼如下:

private void btnSerialize_Click(object sender, EventArgs e)
{
    Order order = new Order(1, DateTime.Now, true);
    order.Items.Add(new OrderItem(new Product("腳踏車", 3000), 2));
    order.Items.Add(new OrderItem(new Product("滑鼠墊", 35.6), 10));

    string jsonStr = JsonHelper.Serialize<Order>(order);
    textBox1.Text = jsonStr;
}

private void btnDeserialize_Click(object sender, EventArgs e)
{
    Order order = JsonHelper.Deserialize<Order>(textBox1.Text);
    textBox1.AppendText("\r\n\r\n反序列化結果:");
    textBox1.AppendText("\r\nOrderID: " + order.ID.ToString());
    textBox1.AppendText("\r\nOrderDate: " + order.OrderDate.ToString("yyyy/MM/dd HH:mm:ss"));
    textBox1.AppendText("\r\nUseCoupon: " + order.UseCoupon.ToString());

    foreach (OrderItem item in order.Items)
    {
        textBox1.AppendText("\r\n==========================");
        textBox1.AppendText("\r\nProduct name: " + item.Product.Name);
        textBox1.AppendText("\r\nPrice: " + item.Product.Price.ToString());
        textBox1.AppendText("\r\nCount: " + item.Count.ToString());                
    }
}

此範例程式須加入組件參考:System.Runtime.Serialization 和 System.ServiceModel.Web。JsonHelper 類別的原始碼請參考前面提到的 Chris Pietschmann 的文章。

執行結果如下圖:



點「Serialize」按鈕時所產生的序列化結果是:

{"ID":1,"Items":[{"Count":2,"Product":{"Name":"腳踏車","Price":3000}},{"Count":10,"Product":{"Name":"滑鼠墊","Price":35.6}}],"OrderDate":"\/Date(1261443680236+0800)\/","UseCoupon":true}

這是標準的 JSON 格式字串。要注意的是,如果你在類別上套用的 attribute 是 [Serializable] 而不是 [DataContract],程式雖然也能執行,但輸出的序列化字串格式會不太一樣,變成這樣:

{"<ID>k__BackingField":1,"k__BackingField":"\/Date(1261443890904+0800)\/","k__BackingField":true,"items":[{"Count":2,"Product":{"Name":"腳踏車","Price":3000}},{"Count":10,"Product":{"Name":"滑鼠墊","Price":35.6}}]}

小結

我有個應用程式是用 BinaryFormatter 來序列化多層巢狀的物件結構,並將物件狀態儲存至外部檔案。後來發現那些要序列化的類別在程式改版時偶爾會有增加或刪除屬性的情形,導致新舊版本的的檔案不相容(反序列化時會出現 exception),而且二進位格式的檔案就像個黑盒子,即使想動點手腳來轉換新舊檔案的格式都很困難。

由於使用 JSON 文字格式,DataContractJsonSerializer 在執行反序列化時的「容錯」程度比二進位格式好很多。我試著把範例程式輸出的 JOSN 字串內容稍微修改,例如,將「,"Price":3000」整個欄位資料刪除,或者調動兩個欄位的順序,然後再執行 Deserialize。結果都很順利,那些消失的欄位所對應的屬性,會被設定成該屬性的型別的預設值(例如 int 就是 0)。這表示程式改版時可以比較放心地增刪類別的屬性,不至於為了跟舊版的資料格式相容而綁手綁腳。

若要使用文字格式,還有 XmlSerializer 可以選擇,但它有些限制(例如:無法處理有實作 IDictionary 的類別),而且就產生的資料大小而言,JSON 也比 XML 精簡些。

所以結論就是:改用 DataContractJsonSerializer。

Post Comments

技術提供:Blogger.