実行環境
VS2019 Version 16.4.5
.NET Framework 4.7.2
使用するSerializer
- DataContractJsonSerailizer
- System.Runtime.Serializationを参照追加すれば使用可能。
NuGetで優秀なライブラリが数多く配布されてはいますが、
ここでは基本的なDataContractJsonSerializerを使用して分かりやすく説明します。
実装方法
まずはSerialize/Deserializeするためのシンプルなコード例を示します。
###実装コード(シンプル)
public class Test
{
public string PublicStringProperty { get; set; } = "Public String Property";
public string PublicStringField = "Public String Field";
string PrivateStringProperty { get; set; } = "Private String Property";
string PrivateStringField = "Private String Field";
}
class Program
{
static void Main(string[] args)
{
var test = new Test();
using (var fs = new FileStream("test.json", FileMode.Create))
{
var serializer = new DataContractJsonSerializer(typeof(Test));
serializer.WriteObject(fs, test);
}
using (var fs = new FileStream("test.json", FileMode.Open))
{
var serializer = new DataContractJsonSerializer(typeof(Test));
var dest = serializer.ReadObject(fs);
}
}
}
###出力結果
{"PublicStringField":"Public String Field","PublicStringProperty":"Public String Property"}
見て分かる通り、public
なプロパティとフィールドだけ出力されています。private
なメンバは出力されません。
また[IgnoreDataMember]
でマークすると、そのメンバだけSerialize/Deserializeできないようになります。
[IgnoreDataMember] // Serialize/Deserializeされなくなる
public string PublicStringProperty { get; set; } = "Public String Property";
プロパティの場合、プロパティ本体がpublic
指定でも、get/set
どちらかがpublic
以外(internal/protected/private
)になるとSerialize/Deserializeできなくなります。
public class Test
{
// Serialize/Deserializeされる
public int Value0 { get; set; }
// Serialize/Deserializeされない
public int Value1 { get; internal set; }
public int Value2 { internal get; set; }
public int Value3 { get; protected set; }
public int Value4 { protected get; set; }
public int Value5 { get; private set; }
public int Value6 { private get; set; }
}
さて、出力できましたが改行されていないのでjsonコードが見づらいです。
段落及びインデントで整形してSerialize/Deserializeできるようにします。
###実装コード(整形あり)
using (var fs = new FileStream("test.json", FileMode.Create))
{
var serializer = new DataContractJsonSerializer(typeof(Test));
var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, " ");
serializer.WriteObject(jsonWriter, test);
jsonWriter.Dispose();
}
using (var fs = new FileStream("test.json", FileMode.Open))
{
var serializer = new DataContractJsonSerializer(typeof(Test));
var jsonReader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max);
var dest = serializer.ReadObject(jsonReader);
jsonReader.Dispose();
}
###出力結果
{
"PublicStringField": "Public String Field",
"PublicStringProperty": "Public String Property"
}
jsonWriter
のDispose
実行を忘れないよう気を付けてください。
(Dispose
処理時にFileStream
を経由して書き込まれるため)
using
で書き込めば安全に出力することができます。
using (var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, " "))
{
serializer.WriteObject(jsonWriter, test);
}
JsonReaderWriterFactory.CreateJsonWriter
の第5引数がインデントに代わる文字です。
半角スペース4文字を設定しています。
XmlDictionaryWriter
(JsonReaderWriterFactory.CreateJsonWriter
で生成したWriter)経由で出力したデータは、記載したコードのようにXmlDictionaryReader
経由で読み取るようにしてください。それ以外の方法で読み込むと例外が発生します。
###DataContractAttributeについて
先程Serialize/Deserializeするクラスには何もAttributeをマークしていませんでした。
よく、DataContractAttribute
をマークしてSerialize/Deserializeする必要がある、と見聞きしますがそんなことはありません。
DataContract
はあくまで明示することにより、Serialize/Deserializeに便利な挙動を提供できる仕組みでしかありません。(Serialize/Deserializeするプロパティ名を変えたり、Serialize/Deserializeの順番を変えたり等)
ここでは、DataContract
及びDataMember
をマークした場合の挙動について説明します。
###実装コード(DataContractを使用した場合)
[DataContract]
public class Test
{
[DataMember]
public string PublicStringProperty { get; set; } = "Public String Property";
public string PublicStringField = "Public String Field";
string PrivateStringProperty { get; set; } = "Private String Property";
[DataMember]
string PrivateStringField = "Private String Field";
}
###出力結果
{
"PrivateStringField": "Private String Field",
"PublicStringProperty": "Public String Property"
}
[DataContract]
でマークしたクラスは、[DataMember]
でマークしたメンバのみSerialize/Deserializeされます。private
であっても、Serialize/Deserializeすることが可能です。
その他、引数付きコンストラクタのみを実装したクラスでもSerialize/Deserialize可能になります。
[DataContract]
をマークしていない場合、
引数付きコンストラクタのみ実装されたクラスはSerialize/Deserializeできません。
(デフォルトコンストラクタを実装すればSerialize/Deserializeできます)
// Serialize/Deserialize可能
[DataContract]
public class Test
{
public Test(int value){}
}
// Serialize/Deserializeで例外が発生する
public class Test
{
public Test(int value){}
}
// Serialize/Deserialize可能
public class Test
{
public Test(){}
public Test(int value){}
}
###DataContractの注意点
[DataContract]
でマークすると、引数付きコンストラクタのみでもSerialize/Deserialize可能でした。
逆に言えば、デフォルトコンストラクタが動作していないということが説明できます。
これは[DataContract]
でマークしたクラスは、Deserialize時に対象クラスをFormatterServices.GetUninitializedObject(Type)
を使用してオブジェクト生成されるからです。
[DataContract]
public class Test
{
[DataMember]
public string PublicStringProperty { get; set; } = "この文字で初期化されない";
}
Deserialize時に、ある特定の値に初期化しておきたい場合は、[OnDeserializing]
をマークしたメソッドを実装します。
[DataContract]
public class Test
{
[DataMember]
public string PublicStringProperty { get; set; };
[OnDeserializing]
void DefaultDeserializing(StreamingContext sc)
{
PublicStringProperty = "Public String Property";
}
}
基本的にコード上でクラス生成されるので、デフォルトコンストラクタが動作して初期化されるとは思います。
しかし、後述する未初期化ケースによって問題になることがあります。(ありました)
###問題パターン
Test.Value
は、エディター上で3の値に編集されて、test.json
として出力されたとします。
[DataContract]
public class Test
{
[DataMember]
public int Value; // 3が入る
}
{
"Value": 3
}
仕様変更が入り、Test.Value
→Test.Value2
と名前が変更されたとします。
役割が同じということだったので、初期値は3が入るようにコードを修正します。
[DataContract]
public class Test
{
[DataMember]
public int Value2 = 3;
}
この状態でtest.json
をDeserializeするとどうなるでしょう。
test.json
には、Value
というフィールドが存在しないため、Value2
の読み込みは行われません。
そしてデフォルトコンストラクタも動作しないので、
このValue2
は3で初期化されず、intの規定値となる0で初期化されます。
仕様変更後の型で仕様変更前のデータをDeserializeする際、こういった不具合の発生原因を作ってしまいます。
この場合、対応方法は各種ありますが、DataContractを使用して仕様変更前のデータをDeserializeしたいのであればDataMember(Name = "")
で以前のフィールド名(プロパティ名)指定してあげれば、Deserializeすることが可能です。
[DataContract]
public class Test
{
[DataMember(Name = "Value")]
public int Value2;
}
このような部分が、DataContractのメリットでもあり、デメリットでもあります。
###Serializeパターン
では、下記のようなパターンはSerialize/Deserializeできるでしょうか。
public class Child
{
public int Value = 0;
}
[DataContract]
public class Parent
{
[DataMember]
public Child ChildValue = new Child();
}
{
"ChildValue": {
"Value": 0
}
}
Rootとなる型だけ、DataContract
をマークしておけばSerialize/Deserialize可能です。
では、継承を用いた少し複雑なパターンはどうでしょうか。
(説明に不要なコードは、使用しないように修正しています)
[DataContract]
public class Super
{
[DataMember]
public int SuperValue = 0;
}
[DataContract]
public class Sub : Super
{
[DataMember]
public int SubValue = 1;
}
class Program
{
static void Main(string[] args)
{
Sub instance = new Sub();
using (var fs = new FileStream("sub.json", FileMode.Create))
{
var serializer = new DataContractJsonSerializer(typeof(Super));
serializer.WriteObject(fs, instance);
}
using (var fs = new FileStream("sub.json", FileMode.Open))
{
var serializer = new DataContractJsonSerializer(typeof(Super));
var dest = serializer.ReadObject(fs);
}
}
}
残念ながら、例外が発生します。(System.Runtime.Serialization.SerializationException
)
基底クラス(Super
)としてSerializeするはずが、型を知らない派生クラス(Sub
)でSerializeしているからです。もちろん、Sub intance
の宣言をSuper instance
にしても、例外は発生します。
派生クラスの型さえ伝えられれば、たとえ基底クラスの型を指定していてもSerializeが可能になります。
(当たり前ですが、継承関係にあることが前提です)
Sub instance = new Sub();
var settings = new DataContractJsonSerializerSettings()
{
KnownTypes = new Type[] { typeof(Sub) }
};
using (var fs = new FileStream("sub.json", FileMode.Create))
{
var serializer = new DataContractJsonSerializer(typeof(Super), settings);
serializer.WriteObject(fs, instance);
}
using (var fs = new FileStream("sub.json", FileMode.Open))
{
var serializer = new DataContractJsonSerializer(typeof(Super), settings);
var dest = serializer.ReadObject(fs);
}
DataContractJsonSerializerSettings.KnownTypes
に、継承関係にあるSub
のTypeを配列で入れます。
これにより、DataContractJsonSerializer
に基底クラスの型を指定しても、継承されたクラスのインスタンスをSerialize/Deserializeすることが可能になります。
###DataContractJsonSerializerSettingsについて
KnownTypes
以外にも設定できる項目があります。
DataContractJsonSerializerSettings.EmitTypeInformation
にEmitTypeInformation.Always
を指定すれば、型情報も出力されるようになります。
namespace CSharp_sandbox
{
[DataContract]
public class Test
{
[DataMember]
public int Value = 1;
}
class Program
{
static void Main(string[] args)
{
var test = new Test();
var settings = new DataContractJsonSerializerSettings()
{
EmitTypeInformation = EmitTypeInformation.Always
};
using (var fs = new FileStream("test.json", FileMode.Create))
{
using (var writer = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, " "))
{
var serializer = new DataContractJsonSerializer(typeof(Test), settings);
serializer.WriteObject(writer, test);
}
}
using (var fs = new FileStream("test.json", FileMode.Open))
{
using (var reader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max))
{
var serializer = new DataContractJsonSerializer(typeof(Test), settings);
var dest = serializer.ReadObject(reader);
}
}
}
}
}
{
"__type": "Test:#CSharp_sandbox",
"Value": 1
}
型名
:#名前空間
という形で出力されます。
DataContractJsonSerializerSettings.UseSimpleDictionaryFormat
をtrue
にすると、
Dictionary<TKey,TValue>
をSerializeする時のデータ形式が変わります。
var settings = new DataContractJsonSerializerSettings()
{
UseSimpleDictionaryFormat = true
};
trueの場合
{
"Dict": {
"TestKey0": 0,
"TestKey1": 1,
"TestKey2": 2
}
}
falseの場合(規定値)
{
"Dict": [
{
"Key": "TestKey0",
"Value": 0
},
{
"Key": "TestKey1",
"Value": 1
},
{
"Key": "TestKey2",
"Value": 2
}
]
}
名前の通り、Key:Value
という形でシンプルにSerializeされるようになります。
#終わりに
自分でもたまにDataContract
について混乱することがあったため、検証がてら記事にしてみました。
この記事が誰かの助けになれば幸いです。
間違い等があれば、ご指摘していただけると助かります。