はじめに
この記事は 株式会社ラグザイア Advent Calendar 2023 の記事です。
先日 XmlSerializer で DefaultValueAttribute を使ったときのモヤっとした動き という記事を書きました。
ありがたいことにコメントを頂けました。
コメントのおかげでモヤっとした動きに対して、ある程度納得することができたので記事にしました。
結果であって目的ではない
「XmlSerializer
を使って DefaultValue
属性で指定した値と同じときは XML で出力されない」というのは、結果的な動きとしては間違ってはいませんが、目的ではないと考えることで納得できました。
この考えになったのは、コメントいただいたように「スキーマがある」前提で考え直してみたからです。
スキーマからクラスを作る前提の手順
- スキーマを作成する
- スキーマにあうようにクラスを定義する
-
XmlSelializer
でシリアライズ、デシリアライズする
このとき、スキーマで要素のデフォルト値を設定したとします。
スキーマでデフォルト値を設定しているので、クラスでも必ず初期値を設定するようにします。
すると、必ず初期値を設定するようにしたので、 Default Value
属性をつけても問題ないはずです。
ここで要素の値をデフォルト値のままシリアライズします。
このとき記事で書いたように、XMLにはデフォルト値の要素の値は出力されません。
この理由は正直分かりません。(コメントにあったように歴史的な背景があるのかもしれません。)
理由は分かりませんが、Default Value
属性がついているということは初期値は担保されていると見なすことができ、デシリアライズ時に初期値がセットされるはずですので、シリアライズ、デシリアライズへの影響は無いと考えることはできそうです。
つまり、「DefaultValue
属性をつけることで、XML書き出ししない」というのは目的ではなく、単なる結果であったと考えれば、 DefaultValue
属性の役割は何も変わっていないので納得できます。
文章ばかりで分かりにくいので実際にやってみます。
スキーマからクラスを作ってXMLを読み書きする
先ほどの手順に従って実際にやっていきます。
とりあえず新規でコンソールアプリケーションを作ります。
XML スキーマを作成する
まずはXMLスキーマを新規作成します。プロジェクトにXMLスキーマファイルを追加します。
XMLスキーマファイルである MySchema.xsd を追加しました。
スキーマの内容ですが、公式のスキーマのサンプル を参考にしてItemがnameをもつ構造としました。また、Nameには初期値として default name
という初期値を設定します。
<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="MySchema"
targetNamespace="http://tempuri.org/MySchema.xsd"
elementFormDefault="qualified"
xmlns="http://tempuri.org/MySchema.xsd"
xmlns:mstns="http://tempuri.org/MySchema.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<xs:element name="Item">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string" default ="default name" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
XMLスキーマからクラスを生成する
XMLスキーマからクラスを生成します。公式にやり方が記載されています。
Visual Studio でコンソールをひらき
xsd ./MyShema.xsd
良く分からなかったのでChatGPTに聞きました。
どうやらクラスを生成するのか、データセットを生成するのか指定してあげる必要があったようです。
/classes
オプションを追加すると良いようです。
xsd /?
でヘルプを見てみると、オプションとして書いてありました。 /c
でも良いようです。
気を取り直して下記を実行します。
xsd ./MyShema.xsd /c
ソリューションエクスプローラーを見ると、スキーマファイルから、MySchema.cs
というファイルが生成されていました。
MySchema.cs
の内容を確認するとこのようになっていました。
//------------------------------------------------------------------------------
// <auto-generated>
// このコードはツールによって生成されました。
// ランタイム バージョン:4.0.30319.42000
//
// このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
// コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------
using System.Xml.Serialization;
//
// このソース コードは xsd によって自動生成されました。Version=4.8.3928.0 です。
//
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/MySchema.xsd")]
[System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/MySchema.xsd", IsNullable=false)]
public partial class Item {
private string nameField;
public Item() {
this.nameField = "default name";
}
/// <remarks/>
public string Name {
get {
return this.nameField;
}
set {
this.nameField = value;
}
}
}
Item のコンストラクタを見ると nameField
がスキーマで設定した初期値でセットされていることが分かります。
これで初期値は必ずセットされることが担保できたので、 DefaultValue
属性を付けても問題ないはずです。Name プロパティに DefaultValue
属性をつけます。
//------------------------------------------------------------------------------
// このコードはツールによって生成されたコードに手作業でNameプロパティに
// DefaultValueAttributeを付与したものです。
// ランタイム バージョン:4.0.30319.42000
//------------------------------------------------------------------------------
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/MySchema.xsd")]
[System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/MySchema.xsd", IsNullable=false)]
public partial class Item {
private string nameField;
public Item() {
this.nameField = "default name";
}
/// <remarks/>
[System.ComponentModel.DefaultValueAttribute("default name")]
public string Name {
get {
return this.nameField;
}
set {
this.nameField = value;
}
}
}
ポイントとしては、「XMLに書き出しする/しないに関する意図は無い」という点です。
XmlSelializer でシリアライズ、デシリアライズする
Item の Name に初期値と同じ default name
をセットしてシリアライズし、XMLで書きだした値をもう一度読み込んで Item の Name の値を確認するコードを書きます。
using System.Xml;
using System.Xml.Serialization;
namespace XmlSchemaSample
{
class Program
{
static void Main()
{
// Nameにデフォルト値を設定しておく
var item = new Item { Name = "default name" };
var serializer = new XmlSerializer(typeof(Item));
// シリアライズ
using (var fs = new FileStream("serialized.xml", FileMode.Create))
{
serializer.Serialize(fs, item);
}
Console.WriteLine("書き込み済み");
// シリアライズしたファイルを読み込んでデシリアライズ
using (FileStream fs = new FileStream("serialized.xml", FileMode.Open))
{
var readItem = serializer.Deserialize(XmlReader.Create(fs)) as Item;
Console.WriteLine(readItem?.Name);
}
}
}
}
実行して、書き込まれたファイル内容を確認します。
<?xml version="1.0" encoding="utf-8"?>
<Item xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://tempuri.org/MySchema.xsd" />
記事で書いたように、Item.Name の値はデフォルト値と同じ default name
と同じなのでXMLに書き出されていません。
XMLに書き出されないことでの影響はデシリアライズ時にあると考えられますが、デシリアライズ後のNameの値は、コンストラクタで設定されるので、ちゃんと初期値通りの default name がセットされていました。
おまけ:コンストラクタで値をセットしないとどうなるか?
コンストラクタの初期値設定部分をコメントアウトした場合どうなるか見てみます。
public Item() {
// this.nameField = "default name";
}
シリアライズの実行結果は変わりませんでした。
デシリアライズの結果は変わりました。Nameの値がセットされておらず空文字が出力されました。スキーマを参照して、いい感じに値をセットしてくれるわけではなさそうです。
まとめ
ということで、「DefaultValueAttribute で指定した値と同じとき、XmlSylializer でシリアライズすると、XMLに出力しない」ことに対する自分なりの結論はこうなりました。
-
DefaultValue
属性自体に XML に出力しないという目的があるわけではない(公式ではオプションと紹介されていますが納得できないので無視)結果的にこうなっているだけ。 - なぜこのような挙動になっているのかのちゃんとした理由は分からない
-
DefaultValue
属性を付けている =>DefaultValue
属性で指定した初期値が必ずセットされることが担保できている前提の挙動