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
}

読み取り専用プロパティ

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

日付

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

キャメル ケース

データ モデルを変更せずにキャメル ケースで 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; } }
}

日付

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

注

オブジェクト参照は 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);
}