ASP.NET Web API 中的 JSON 與 XML 序列化

本文將介紹 ASP.NET Web API 中的 JSON 和 XML 格式器。

在 ASP.NET Web API 中,媒體類型格式器是一個物件,它可以:

  • 從 HTTP 訊息本文讀取 CLR 物件
  • 將 CLR 物件寫入 HTTP 訊息本文

Web API 提供了 JSON 和 XML 的媒體類型格式器。 預設情況下,架構會將這些格式器插入管線中。 用戶端可以在 HTTP 請求的 Accept 標頭中請求 JSON 或 XML。

目錄

JSON 媒體類型格式器

JSON 格式由 JsonMediaTypeFormatter 類別提供。 預設情況下,JsonMediaTypeFormatter 使用 Json.NET 程式庫來執行序列化。 Json.NET 是一個第三方開源專案。

如果您願意,可以將 JsonMediaTypeFormatter 類別設定為使用 DataContractJsonSerializer 而不是 Json.NET。 若要這麼做,請將 UseDataContractJsonSerializer 屬性設為 True:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.UseDataContractJsonSerializer = true;

JSON 序列化

使用預設的 Json.NET 序列化器,本節描述了 JSON 格式化器的一些特定行為。 這並不是 Json.NET 程式庫的完整文件;有關更多資訊,請參閱 Json.NET 文件。

哪些東西會序列化?

預設情況下,所有公共屬性和欄位都包含在序列化的 JSON 中。 若要省略屬性或欄位,請使用 JsonIgnore 屬性對其進行修飾。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    [JsonIgnore]
    public int ProductCode { get; set; } // omitted
}

如果您喜歡「選擇加入」方法,請使用 DataContract 屬性修飾該類別。 如果存在此屬性,則成員將被忽略,除非它們具有 DataMember。 您也可以使用 DataMember 來序列化私人成員。

[DataContract]
public class Product
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public decimal Price { get; set; }
    public int ProductCode { get; set; }  // omitted by default
}

唯讀屬性

預設情況下,唯讀屬性是序列化的。

日期

預設情況下,Json.NET 以 ISO 8601 格式寫入日期。 UTC (協調世界時) 中的日期帶有「Z」尾碼。 當地時間日期包含時區偏移。 例如:

2012-07-27T18:51:45.53403Z         // UTC
2012-07-27T11:51:45.53403-07:00    // Local

預設情況下,Json.NET 會保留時區。 您可以透過設定 DateTimeZoneHandling 屬性來重設此設定:

// Convert all dates to UTC
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;

如果您偏好使用 Microsoft JSON 日期格式 (例如 "\/Date(ticks)\/") 而非 ISO 8601,請在序列化設定中將 DateFormatHandling 屬性設置為:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateFormatHandling 
= Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;

縮排

若要編寫縮排的 JSON,請將 Formatting 設定為 Formatting.Indented:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

駝峰式大小寫

若要使用駝峰式大小寫來寫 JSON 屬性名稱,而不變更資料模型,請在序列化程式上設定 CamelCasePropertyNamesContractResolver:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

匿名和弱型別物件

動作方法可以傳回匿名物件,並將其序列化為 JSON。 例如:

public object Get()
{
    return new { 
        Name = "Alice", 
        Age = 23, 
        Pets = new List<string> { "Fido", "Polly", "Spot" } 
    };
}

回應訊息本文將包含以下 JSON:

{"Name":"Alice","Age":23,"Pets":["Fido","Polly","Spot"]}

如果您的 Web API 從用戶端接收鬆散結構的 JSON 物件,您可以將請求本文還原序列化為 Newtonsoft.Json.Linq.JObject 類型。

public void Post(JObject person)
{
    string name = person["Name"].ToString();
    int age = person["Age"].ToObject<int>();
}

但是,通常最好使用強型別資料物件。 這樣您就不需要自己解析資料,並且可以獲得模型驗證的好處。

XML 序列化程式不支援匿名型別或 JObject 執行個體。 如果您對 JSON 資料使用這些功能,則應從管線中刪除 XML 格式器,如本文後面所述。

XML 媒體類型格式器

XML 格式由 XmlMediaTypeFormatter 類別提供。 預設情況下,XmlMediaTypeFormatter 使用 DataContractSerializer 類別來執行序列化。

如果您想的話,可以將 XmlMediaTypeFormatter 設定為使用 XmlSerializer 而不是 DataContractSerializer。 若要這樣做,請將 UseXmlSerializer 屬性設為 True:

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.UseXmlSerializer = true;

XmlSerializer 類別支援比 DataContractSerializer 更窄的類型集,但對產生的 XML 提供更多控制。 如果需要符合現有的 XML 架構,請考慮使用 XmlSerializer。

XML 序列化

本節使用預設的 DataContractSerializer 來描述 XML 格式器的一些特定行為。

預設情況下,DataContractSerializer 的行為如下:

  • 所有公共讀取/寫入屬性和欄位均已序列化。 若要省略屬性或欄位,請使用 IgnoreDataMember 屬性進行修飾。
  • 私人和受保護的成員不會被序列化。
  • 唯讀屬性不會序列化。 (但唯讀集合屬性的內容會序列化。)
  • 類別和成員名稱在 XML 中的編寫與類別宣告中的顯示完全相同。
  • 使用預設的 XML 命名空間。

如果需要對序列化進行更多控制,可以使用 DataContract 屬性來修飾該類別。 當此屬性存在時,類別的序列化方式如下:

  • 「選擇加入」方法:預設情況下,屬性和欄位不會序列化。 若要序列化屬性或欄位,請使用 DataMember 屬性對其進行修飾。
  • 若要序列化私人或受保護的成員,請使用 DataMember 屬性對其進行修飾。
  • 唯讀屬性不會序列化。
  • 若要變更類別名稱在 XML 中的顯示方式,請在 DataContract 屬性中設定 Name 參數。
  • 若要變更成員名稱在 XML 中的顯示方式,請在 DataMember 屬性中設定 Name 參數。
  • 若要變更 XML 命名空間,請在 DataContract 類別中設定 Namespace 參數。

唯讀屬性

唯讀屬性不會序列化。 如果唯讀屬性具有備份私人欄位,則可以使用 DataMember 屬性標記該私人欄位。 此方法需要類別上的 DataContract 屬性。

[DataContract]
public class Product
{
    [DataMember]
    private int pcode;  // serialized

    // Not serialized (read-only)
    public int ProductCode { get { return pcode; } }
}

日期

日期以 ISO 8601 格式寫入。 例如,「2012-05-23T20:21:37.9116538Z」。

縮排

若要編寫縮排的 XML,請將 Indent 屬性設為 True:

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.Indent = true;

設定針對每一類型的 XML 序列化器

您可以為不同的 CLR 類型設定不同的 XML 序列化程式。 例如,您可能有一個特定的資料物件需要使用 XmlSerializer 才能保持向後相容。 您可以對此物件使用 XmlSerializer,並繼續對其他類型使用 DataContractSerializer。

若要為特定類型設定 XML 序列化程式,請呼叫 SetSerializer。

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
// Use XmlSerializer for instances of type "Product":
xml.SetSerializer<Product>(new XmlSerializer(typeof(Product)));

您可以指定 XmlSerializer 或任何衍生自 XmlObjectSerializer 的物件。

移除 JSON 或 XML 格式器

如果您不想使用 JSON 格式器或 XML 格式器,可以將其從格式器清單中移除。 這樣做的主要原因是:

  • 將您的 Web API 回應限制為特定媒體類型。 例如,您可能決定只支援 JSON 回應,並移除 XML 格式器。
  • 用自訂格式器取代預設格式器。 例如,您可以將 JSON 格式器替換為您自己的 JSON 格式器自訂的實作版本。

以下程式碼顯示如何移除預設格式器。 從 Global.asax 中定義的Application_Start方法來呼叫此功能。

void ConfigureApi(HttpConfiguration config)
{
    // Remove the JSON formatter
    config.Formatters.Remove(config.Formatters.JsonFormatter);

    // or

    // Remove the XML formatter
    config.Formatters.Remove(config.Formatters.XmlFormatter);
}

處理循環物件參考

預設情況下,JSON 和 XML 格式器會將所有物件寫入為值。 如果兩個屬性參考同一個物件,或者同一個對像在集合中出現兩次,則格式器將序列化該物件兩次。 如果您的物件圖包含循環,這是一個特殊的問題,因為序列化程式在偵測到圖中的循環時會擲回例外狀況。

考慮以下物件模型和控制器。

public class Employee
{
    public string Name { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public string Name { get; set; }
    public Employee Manager { get; set; }
}

public class DepartmentsController : ApiController
{
    public Department Get(int id)
    {
        Department sales = new Department() { Name = "Sales" };
        Employee alice = new Employee() { Name = "Alice", Department = sales };
        sales.Manager = alice;
        return sales;
    }
}

叫用此動作會導致格式器拋出例外狀況,這會轉換為狀態代碼 500 (內部伺服器錯誤) 回應用戶端。

若要保留 JSON 中的物件參考,請將下列程式碼新增至 Global.asax 檔案中的 Application_Start 方法:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.All;

現在控制器動作將傳回如下所示的 JSON:

{"$id":"1","Name":"Sales","Manager":{"$id":"2","Name":"Alice","Department":{"$ref":"1"}}}

請注意,序列化程式向兩個物件新增了「$id」屬性。 此外,它偵測到 Employee.Department 屬性建立了一個循環,因此它用物件參考取代該值:{"$ref":"1"}。

注意

在 JSON 中,物件參考並不是標準做法。 在使用此功能之前,請考慮您的用戶端是否能夠解析結果。 簡單地從圖中移除循環可能會更好。 例如,在這個範例中,從員工到部門的連結實際上並不是必需的。

若要在 XML 中保留物件參考,您有兩種選擇。 更簡單的選擇是新增 [DataContract(IsReference=true)] 到您的模型類別中。 IsReference 參數會啟用物件參考。 請記住,DataContract 使序列化為選擇性加入,因此您還需要將 DataMember 屬性添加到該項目的屬性中:

[DataContract(IsReference=true)]
public class Department
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public Employee Manager { get; set; }
}

現在格式器將產生類似以下內容的 XML:

<Department xmlns:i="http://www.w3.org/2001/XMLSchema-instance" z:Id="i1" 
            xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" 
            xmlns="http://schemas.datacontract.org/2004/07/Models">
  <Manager>
    <Department z:Ref="i1" />
    <Name>Alice</Name>
  </Manager>
  <Name>Sales</Name>
</Department>

如果您希望在模型類上避免使用屬性,還有另一種選擇:建立一個特定類型的 DataContractSerializer 執行個體,並在建構函式中將 preserveObjectReferences 設為 True。 然後將此執行個體設置為 XML 媒體類型格式器上的每類型的序列化器。 以下程式碼展示如何執行此動作:

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
var dcs = new DataContractSerializer(typeof(Department), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ true, null);
xml.SetSerializer<Department>(dcs);

測試物件序列化

在設計 Web API 時,測試資料物件的序列化方式非常有用。 您無需建立控制器或叫用控制器動作即可執行此動作。

string Serialize<T>(MediaTypeFormatter formatter, T value)
{
    // Create a dummy HTTP Content.
    Stream stream = new MemoryStream();
    var content = new StreamContent(stream);
    /// Serialize the object.
    formatter.WriteToStreamAsync(typeof(T), value, stream, content, null).Wait();
    // Read the serialized string.
    stream.Position = 0;
    return content.ReadAsStringAsync().Result;
}

T Deserialize<T>(MediaTypeFormatter formatter, string str) where T : class
{
    // Write the serialized string to a memory stream.
    Stream stream = new MemoryStream();
    StreamWriter writer = new StreamWriter(stream);
    writer.Write(str);
    writer.Flush();
    stream.Position = 0;
    // Deserialize to an object of type T
    return formatter.ReadFromStreamAsync(typeof(T), stream, null, null).Result as T;
}

// Example of use
void TestSerialization()
{
    var value = new Person() { Name = "Alice", Age = 23 };

    var xml = new XmlMediaTypeFormatter();
    string str = Serialize(xml, value);

    var json = new JsonMediaTypeFormatter();
    str = Serialize(json, value);

    // Round trip
    Person person2 = Deserialize<Person>(json, str);
}