XmlSerializerクラスでシリアライズ/デシリアライズを行うと、デシリアライズに失敗することがある。その回避策を含め、XmlSerializerクラスの使い方を説明する。
オブジェクトをシリアライズする(ファイルなどに書き込める形にする)/デシリアライズする(シリアライズしたものから元のオブジェクトを復元する)ために、.NET Frameworkにはさまざまな方法が用意されている。その中でXmlSerializerクラス(System.Xml.Serialization名前空間)は、シリアライズ可能な型がわりとプリミティブなものに制限されてはいるものの、シリアライズ結果がXMLフォーマットの文字列で可読性に優れていることから、アプリの設定を保存するというような用途でよく使われている。
本稿では、XmlSerializerクラスを使ったシリアライズ/デシリアライズの方法と注意点を解説する。
なお、XmlSerializerクラスは.NET Frameworkのバージョン1.1からあるものだが、本稿はそれ以降の内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015(またはそれ以降)が必要である。
次のサンプルコードのようにする。ポイントは、デシリアライズ時にXmlReaderクラス(System.Xml名前空間)を使い、適切なXmlReaderSettingsオブジェクト(System.Xml名前空間)を与えることだ(その理由は後述する)。
using System.IO;
using System.Text;
using System.Xml.Serialization;
using static System.Console;
// シリアライズ対象のクラス
public class Sample
{
public int Id { get; set; }
public string Text { get; set; }
}
class Program
{
static void Main(string[] args)
{
// シリアライズ先のファイル
const string xmlFile = @".\Sample.xml";
// シリアライズするオブジェクト
var obj = new Sample { Id = 7, Text = "@IT" }; // (1)
// シリアライズする
var xmlSerializer1 = new XmlSerializer(typeof(Sample));
using (var streamWriter = new StreamWriter(xmlFile, false, Encoding.UTF8))
{
xmlSerializer1.Serialize(streamWriter, obj);
streamWriter.Flush();
}
// 書き出されたファイルの内容(一部に改行を入れている):
// <?xml version="1.0" encoding="utf-8"?>
// <Sample xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xmlns:xsd="http://www.w3.org/2001/XMLSchema">
// <Id>7</Id>
// <Text>@IT</Text>
// </Sample>
// デシリアライズする
var xmlSerializer2 = new XmlSerializer(typeof(Sample));
Sample result;
var xmlSettings = new System.Xml.XmlReaderSettings()
{
CheckCharacters = false, // (2)
};
using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8))
using (var xmlReader
= System.Xml.XmlReader.Create(streamReader, xmlSettings))
{
result = (Sample)xmlSerializer2.Deserialize(xmlReader); // (3)
}
WriteLine($"{result.Id}, {result.Text}");
// 出力:7, @IT
#if DEBUG
ReadKey();
#endif
}
}
Imports System.Console
Imports System.IO
Imports System.Text
Imports System.Xml.Serialization
' シリアライズ対象のクラス
Public Class Sample
Public Property Id As Integer
Public Property Text As String
End Class
Module Module1
Sub Main()
' シリアライズ先のファイル
Const xmlFile As String = ".\Sample.xml"
' シリアライズするオブジェクト
Dim obj = New Sample With {.Id = 7, .Text = "@IT"} ' (1)
' シリアライズする
Dim xmlSerializer1 = New XmlSerializer(GetType(Sample))
Using streamWriter = New StreamWriter(xmlFile, False, Encoding.UTF8)
xmlSerializer1.Serialize(streamWriter, obj)
streamWriter.Flush()
End Using
' 書き出されたファイルの内容(一部に改行を入れている):
' <?xml version="1.0" encoding="utf-8"?>
' <Sample xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
' xmlns:xsd="http://www.w3.org/2001/XMLSchema">
' <Id>7</Id>
' <Text>@IT</Text>
' </Sample>
' デシリアライズする
Dim xmlSerializer2 = New XmlSerializer(GetType(Sample))
Dim result As Sample
Dim xmlSettings = New System.Xml.XmlReaderSettings() _
With {
.CheckCharacters = False ' (2)
}
Using streamReader = New StreamReader(xmlFile, Encoding.UTF8)
Using xmlReader = System.Xml.XmlReader.Create(streamReader, xmlSettings)
result = CType(xmlSerializer2.Deserialize(xmlReader), Sample) ' (3)
End Using
End Using
WriteLine($"{result.Id}, {result.Text}")
' 出力:7, @IT
#If DEBUG Then
ReadKey()
#End If
End Sub
End Module
前掲のコードでXmlReaderクラスを使っている理由は、データの中にXMLとして不正な文字が入っていた場合に対処するためである。
XMLで使用できる文字は、決められている。規格にない文字は、XMLとしては不正なのである。例えば、TAB/CR/LFを除く制御コードは不正なのだ。
XMLとして不正な文字が入っているときに、XmlReaderクラスを使わずにStreamReaderオブジェクトをXmlSerializerオブジェクトに直接渡したり(すると、XmlSerializerクラスは内部的にXmlReaderクラスを既定の状態で生成して使用する)、あるいは、XmlReaderオブジェクトを作るときに適切なXmlReaderSettingsオブジェクトを渡さないと、デシリアライズに失敗して例外が発生する。
そのことを、先のコードを書き換えて確かめてみよう。
まず、XMLとして不正な文字をデータに入れる。コメント「(1)」のところを、次のように変更する。「\u001a」「ChrW(&H1A)」とは制御コードEOFのことだ(昔はテキストファイルの終端記号としてよく使われた)。
// シリアライズするオブジェクト
// var obj = new Sample { Id = 7, Text = "@IT" }; // (1)
// ↓
var obj = new Sample { Id = 7, Text = "@\u001aIT" };
' シリアライズするオブジェクト
' Dim obj = New Sample With {.Id = 7, .Text = "@IT"} ' (1)
' ↓
Dim obj = New Sample With {.Id = 7, .Text = $"@{ChrW(&H1A)}IT"}
次に、コメント「(2)」と付けてある行をコメントアウトする(次のコード)。「CheckCharacters = false」を指定しないということは、XMLとして不正な文字をチェックさせるということだ。チェックするのが、XmlReaderクラスとXmlSerializerクラスの既定の動作である(そして、XmlSerializerクラスにはこの動作を変更する方法が用意されておらず、XmlSerializerクラスに与えるXmlReaderクラスの側で動作を変更しなければならない)。
var xmlSettings = new System.Xml.XmlReaderSettings()
{
//CheckCharacters = false, // (2)
};
Dim xmlSettings = New System.Xml.XmlReaderSettings() _
'With {
' .CheckCharacters = False ' (2)
'}
これで実行してみよう。シリアライズは成功して、ファイルにも次のように書き出される。
<?xml version="1.0" encoding="utf-8"?>
<Sample xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Id>7</Id>
<Text>@IT</Text>
</Sample>
しかし、デシリアライズはコメント「(3)」のところで例外が発生する(次の画像)。
この例外に遭遇する確率は、恐らくずいぶんと小さいだろう。シリアライズする元データにXMLとして不正な文字が入ってくることはまずないからだ。気を付けていないと、テスト時に不具合を発見できず、何年も運用してから不具合が出るなどということにもなりかねない。「XmlSerializerでデシリアライズするときはXmlReaderを使って不正な文字を許容させる」と覚えておこう。
最後に、コメント「(2)」の行の先頭に付けたコメントを外して「CheckCharacters = false」の指定を有効にし、もう一度実行してみよう。今度はデシリアライズにも成功するはずだ。正しくデシリアライズされたかどうかは、「foreach(var c in result.Text) WriteLine($"{(int)(c):X4}");」(C#)などとして確認できる。
「シリアライズ時にもXmlWriterオブジェクトを渡すようにして、対称にした方がよいのでは?」と思われた読者もいるかもしれない。もちろんそうしても構わない。
ただし、XmlSerializerクラスのソースコードを見ると分かるが、Streamオブジェクトを渡したとき、内部的にはXmlTextReaderオブジェクト/XmlTextWriterオブジェクトが生成される。内部的にXmlTextWriterオブジェクトに設定しているオプション(ソースコードの299行目付近)とは異なる設定を与えたい場合に限って、シリアライズ時にXmlWriterオブジェクトを渡す意味がある(可読性の観点から、意味はなくてもXmlWriterオブジェクトを渡すようにするという判断も大いにありだと思う)。
なお、XmlReaderSettingsクラス/XmlWriterSettingsクラスは.NET Framework 2.0で導入されたものであるため、.NET Framework 1.1からあるXmlSerializerクラスでは使われていない。そのため、XmlSerializerクラスのデシリアライズ時には、空白などを適切に読み飛ばすために「xmlReader.Normalization = true;」としている(ソースコードの376行目付近)。しかしこの設定により、XMLとして不正な文字もチェックするようになるため、シリアライズできたオブジェクトがデシリアライズできないこともあるという非対称性が生じているのである。
XmlSerializerクラスは.NET Frameworkのバージョン1.1からあるため、デシリアライズの結果はObject型で返される。いちいちキャストするのは煩わしいものだ。また、あちこちにデシリアライズするコードを書いていると上述の注意点を忘れてしまうかもしれない。
そこで、次のコードのようにジェネリックなメソッドとしてまとめておくとよいだろう。せっかくの機会なので、非同期バージョンにしておいた。
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
……省略……
// 排他ロックに使うSemaphoreSlimオブジェクト
// (プロセス間の排他が必要なときはSemaphoreオブジェクトに変える)
static System.Threading.SemaphoreSlim _semaphore
= new System.Threading.SemaphoreSlim(1, 1);
// シリアライズする
static async Task SerializeAsync<T>(T data, string filePath)
{
await _semaphore.WaitAsync(); // ロックを取得する
try
{
var xmlSerializer = new XmlSerializer(typeof(T));
using (var streamWriter = new StreamWriter(filePath, false, Encoding.UTF8))
{
await Task.Run(() => xmlSerializer.Serialize(streamWriter, data));
await streamWriter.FlushAsync(); // .NET Framework 4.5以降
}
}
finally
{
_semaphore.Release(); // ロックを解放する
}
}
// デシリアライズする
static async Task<T> DeserializeAsync<T>(string filePath)
{
await _semaphore.WaitAsync(); // ロックを取得する
try
{
var xmlSerializer = new XmlSerializer(typeof(T));
var xmlSettings = new System.Xml.XmlReaderSettings()
{
CheckCharacters = false,
};
using (var streamReader = new StreamReader(filePath, Encoding.UTF8))
using (var xmlReader = System.Xml.XmlReader.Create(streamReader, xmlSettings))
{
return await Task.Run(() => (T)xmlSerializer.Deserialize(xmlReader));
}
}
finally
{
_semaphore.Release(); // ロックを解放する
}
}
Imports System.IO
Imports System.Text
Imports System.Xml.Serialization
……省略……
' 排他ロックに使うSemaphoreSlimオブジェクト
' (プロセス間の排他が必要なときはSemaphoreオブジェクトに変える)
Private _semaphore As System.Threading.SemaphoreSlim _
= New System.Threading.SemaphoreSlim(1, 1)
'シリアライズする
Async Function SerializeAsync(Of T)(data As T, filePath As String) As Task
Await _semaphore.WaitAsync() ' ロックを取得する
Try
Dim XmlSerializer = New XmlSerializer(GetType(T))
Using streamWriter = New StreamWriter(filePath, False, Encoding.UTF8)
Await Task.Run(Sub() XmlSerializer.Serialize(streamWriter, data))
Await streamWriter.FlushAsync() ' .NET Framework 4.5以降
End Using
Finally
_semaphore.Release() ' ロックを解放する
End Try
End Function
' デシリアライズする
Async Function DeserializeAsync(Of T)(filePath As String) As Task(Of T)
Await _semaphore.WaitAsync() ' ロックを取得する
Try
Dim xmlSerializer = New XmlSerializer(GetType(T))
Dim xmlSettings = New System.Xml.XmlReaderSettings() _
With {
.CheckCharacters = False
}
Using streamReader = New StreamReader(filePath, Encoding.UTF8)
Using xmlReader = System.Xml.XmlReader.Create(streamReader, xmlSettings)
Return Await Task.Run(Function() CType(xmlSerializer.Deserialize(xmlReader), T))
End Using
End Using
Finally
_semaphore.Release() ' ロックを解放する
End Try
End Function
XmlSerializerクラスでデシリアライズするときは、XMLファイルの読み込みにXmlReaderクラスを明示的に使う。XmlReaderオブジェクトを作るときには、「CheckCharacters = false」オプションを忘れずに付ける。
利用可能バージョン:.NET Framework 1.0以降(サンプルコードにはそれ以降の機能/構文も含む)
カテゴリ:クラス・ライブラリ 処理対象:シリアライズ
使用ライブラリ:XmlSerializerクラス(System.Xml.Serialization名前空間)
使用ライブラリ:XmlReaderクラス(System.Xml名前空間)
使用ライブラリ:XmlReaderSettingsクラス(System.Xml名前空間)
関連TIPS:ファイルにテキストを書き込むには?[C#、VB]
関連TIPS:テキスト・ファイルの内容を読み込むには?[C#、VB]
関連TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.