ASP.NET Web API での JSON と XML シリアル化

この記事では、ASP.NET Web API での JSON フォーマッタと XML フォーマッタについて説明します。

ASP.NET Web API では、"メディアタイプ フォーマッタ" は、以下を行えるオブジェクトです。

  • HTTP メッセージ本文から CLR オブジェクトを読み取る
  • HTTP メッセージ本文に CLR オブジェクトを書き込む

Web API は、JSON と XML の両方のメディアタイプ フォーマッタを提供します。 フレームワークは、既定でこれらのフォーマッタをパイプラインに挿入します。 クライアントは、HTTP 要求の Accept ヘッダーで JSON または XML を要求できます。

内容

JSON メディアタイプ フォーマッタ

JSON の書式設定は、JsonMediaTypeFormatter クラスによって指定されます。 既定では、JsonMediaTypeFormatterJson.NET ライブラリを使用してシリアル化を実行します。 Json.NET は、サードパーティ製のオープンソース プロジェクトです。

必要に応じて、Json.NET の代わりに DataContractJsonSerializer を使用するように JsonMediaTypeFormatter クラスを構成できます。 これを行うには、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
}

読み取り専用プロパティ

読み取り専用プロパティは、既定でシリアル化されます。

Dates

既定では、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;

ISO 8601 の代わりに Microsoft JSON 日付形式 ("\/Date(ticks)\/") を使用する場合は、シリアライザー設定で DateFormatHandling プロパティを設定します。

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

インデント

インデントされた JSON を書き込むには、書式設定の設定を Formatting.Indented に設定します。

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

camel 規約に従った大文字小文字の使い分け

データ モデルを変更せずに camel による大文字小文字の区別で 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 クラスによって指定されます。 既定では、XmlMediaTypeFormatterDataContractSerializer クラスを使用してシリアル化を実行します。

必要に応じて、DataContractSerializer の代わりに XmlSerializer を使用するように XmlMediaTypeFormatter を構成できます。 これを行うには、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; } }
}

Dates

日付は 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 フォーマッタはすべてのオブジェクトを値として書き込みます。 2 つのプロパティが同じオブジェクトを参照する場合、または同じオブジェクトがコレクションに 2 回表示される場合、フォーマッタはオブジェクトを 2 回シリアル化します。 これは、オブジェクト グラフにサイクルが含まれている場合に特に問題になります。シリアライザーがグラフ内のループを検出したときに例外をスローするためです。

次のオブジェクト モデルとコントローラーについて考えてみましょう。

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"} に置き換えます。

Note

オブジェクト参照は JSON では標準ではありません。 この機能を使用する前に、クライアントが結果を解析できるかどうかを検討してください。 グラフからサイクルを削除するだけの方が良い場合があります。 たとえば、この例では、従業員から部署へのリンクは実際には必要ありません。

XML でオブジェクト参照を保持するには、2 つのオプションがあります。 より簡単なオプションは、モデル クラスに [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 インスタンスを作成し、コンストラクターで preserveObjectReferencestrue に設定します。 次に、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);
}