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 クラスによって指定されます。 既定では、JsonMediaTypeFormatter は Json.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 クラスによって指定されます。 既定では、XmlMediaTypeFormatter は DataContractSerializer クラスを使用してシリアル化を実行します。
必要に応じて、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 インスタンスを作成し、コンストラクターで 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);
}