共用方式為


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 格式化器的一些特定行為,使用的是預設的 Json.NET 序列化程式。 這並不是 Json.NET 程式庫的綜合文件;而是 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);
}