【解決編】XmlSerializer で DefaultValueAttribute を使ったときのモヤっとした動き

# はじめに

この記事は [株式会社ラグザイア Advent Calendar 2023](https://qiita.com/advent-calendar/2023/luxiar) の記事です。

先日 [XmlSerializer で DefaultValueAttribute を使ったときのモヤっとした動き](https://qiita.com/lx-sasabo/items/c72f8dada6c02b36fe5c) という記事を書きました。
ありがたいことにコメントを頂けました。
コメントのおかげでモヤっとした動きに対して、ある程度納得することができたので記事にしました。

# 結果であって目的ではない

「`XmlSerializer` を使って `DefaultValue` 属性で指定した値と同じときは XML で出力されない」というのは、結果的な動きとしては間違ってはいませんが、目的ではないと考えることで納得できました。

この考えになったのは、コメントいただいたように「スキーマがある」前提で考え直してみたからです。

スキーマからクラスを作る前提の手順
1. スキーマを作成する
1. スキーマにあうようにクラスを定義する
1. `XmlSelializer` でシリアライズ、デシリアライズする

このとき、スキーマで要素のデフォルト値を設定したとします。
スキーマでデフォルト値を設定しているので、クラスでも必ず初期値を設定するようにします。
すると、必ず初期値を設定するようにしたので、 `Default Value` 属性をつけても問題ないはずです。

ここで要素の値をデフォルト値のままシリアライズします。
このとき[記事](https://qiita.com/lx-sasabo/items/c72f8dada6c02b36fe5c)で書いたように、XMLにはデフォルト値の要素の値は出力されません。
この理由は正直分かりません。（コメントにあったように歴史的な背景があるのかもしれません。）

理由は分かりませんが、`Default Value` 属性がついているということは初期値は担保されていると見なすことができ、デシリアライズ時に初期値がセットされるはずですので、シリアライズ、デシリアライズへの影響は無いと考えることはできそうです。

つまり、「`DefaultValue` 属性をつけることで、XML書き出ししない」というのは目的ではなく、単なる結果であったと考えれば、 `DefaultValue` 属性の役割は何も変わっていないので納得できます。

文章ばかりで分かりにくいので実際にやってみます。

# スキーマからクラスを作ってXMLを読み書きする

先ほどの手順に従って実際にやっていきます。
とりあえず新規でコンソールアプリケーションを作ります。

## XML スキーマを作成する

まずはXMLスキーマを新規作成します。プロジェクトにXMLスキーマファイルを追加します。
![Monosnap 新しい項目の追加 - XmlSchemaSample 2023-12-01 13..png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/71986505-6fa6-d15a-60bd-0c2151cdd431.png)

XMLスキーマファイルである MySchema.xsd を追加しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/db00f0d1-8c33-561a-4361-a6f49b2d7d0c.png)

スキーマの内容ですが、[公式のスキーマのサンプル](https://learn.microsoft.com/ja-jp/dotnet/standard/data/xml/inferring-schemas-from-xml-documents#inferring-schemas-from-xml-documents-1) を参考にしてItemがnameをもつ構造としました。また、Nameには初期値として `default name` という初期値を設定します。

```xml
<?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スキーマからクラスを生成します。公式にやり方が記載されています。

https://learn.microsoft.com/ja-jp/dotnet/standard/serialization/xml-schema-def-tool-gen#to-generate-classes-that-conform-to-a-specific-schema

Visual Studio でコンソールをひらき

```
xsd ./MyShema.xsd
```

を実行します。失敗しました。
![Monosnap XmlSchemaSample - Microsoft Visual Studio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/a6cfbb42-a670-8181-8fdc-9d6ccf58b025.png)

良く分からなかったのでChatGPTに聞きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/44986716-5dc7-08fc-1c64-538a5bc239fb.png)

どうやらクラスを生成するのか、データセットを生成するのか指定してあげる必要があったようです。
`/classes` オプションを追加すると良いようです。
`xsd /?` でヘルプを見てみると、オプションとして書いてありました。 `/c` でも良いようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/0545284a-b15d-85d9-843f-0a643c2b26e4.png)

気を取り直して下記を実行します。
```
xsd ./MyShema.xsd /c
```

成功しました。
![Monosnap XmlSchemaSample - Microsoft Visual Studio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/268afe39-b466-77fe-9a75-ba2fdb7812cc.png)

ソリューションエクスプローラーを見ると、スキーマファイルから、`MySchema.cs` というファイルが生成されていました。

![Monosnap XmlSchemaSample - Microsoft Visual Studio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/2be98976-546e-f5a4-6ad5-5127932d8125.png)

`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` がスキーマで設定した初期値でセットされていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/96be5dd5-476e-83a0-426b-29bfb3041295.png)

これで初期値は必ずセットされることが担保できたので、 `DefaultValue` 属性を付けても問題ないはずです。Name プロパティに `DefaultValue` 属性をつけます。

```MySchema.cs
//------------------------------------------------------------------------------
//     このコードはツールによって生成されたコードに手作業で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 の値を確認するコードを書きます。

```program.cs
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
<?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" />
```

[記事](https://qiita.com/lx-sasabo/items/c72f8dada6c02b36fe5c)で書いたように、Item.Name の値はデフォルト値と同じ `default name` と同じなのでXMLに書き出されていません。
XMLに書き出されないことでの影響はデシリアライズ時にあると考えられますが、デシリアライズ後のNameの値は、コンストラクタで設定されるので、ちゃんと初期値通りの default name がセットされていました。

![Monosnap ~_works_experiment_XmlSchemaSample 2023-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/00c0fe09-bcb7-9a0e-fc0b-6dcc6533d107.png)

## おまけ：コンストラクタで値をセットしないとどうなるか？

コンストラクタの初期値設定部分をコメントアウトした場合どうなるか見てみます。

```cs
    public Item() {
        // this.nameField = "default name";
    }
```

シリアライズの実行結果は変わりませんでした。
デシリアライズの結果は変わりました。Nameの値がセットされておらず空文字が出力されました。スキーマを参照して、いい感じに値をセットしてくれるわけではなさそうです。
![Monosnap ~_works 2023-12-01 17.45.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/3d6b7767-d0df-73c2-6b67-245050327cc4.png)

# まとめ

ということで、「DefaultValueAttribute で指定した値と同じとき、XmlSylializer でシリアライズすると、XMLに出力しない」ことに対する自分なりの結論はこうなりました。

* `DefaultValue` 属性自体に XML に出力しないという目的があるわけではない（[公式ではオプションと紹介](https://learn.microsoft.com/ja-jp/dotnet/api/system.xml.serialization.xmlserializer?view=net-8.0)されていますが納得できないので無視）結果的にこうなっているだけ。
* なぜこのような挙動になっているのかのちゃんとした理由は分からない
* `DefaultValue` 属性を付けている => `DefaultValue` 属性で指定した初期値が必ずセットされることが担保できている前提の挙動