C#でJSON形式のファイルや文字列を読み込んでオブジェクトにデシリアライズする、なんてのは頻出課題の1つですね。少し前ならNewtonJSONを、最新ならSystem.Text.Jsonを使って行うのが常套手段かと思います。私はまぁ少し前から稼働しているプロジェクトに対して上位互換機能を付けようとしていたのでNewtonJSONパッケージを使っていたわけですが、そこでついつい(JSON形式で指定していないオブジェクトのプロパティに対してはデフォルト値をgetterで返すようにしよう・・・)などと要らぬ下心というのか、調子こいたというか、そういう指定をしてしまってまさかのハマりを経験してしまいました。
NewtonJSONは大変便利で、大抵のプロパティはちゃんとシリアライズ、デシリアライズしてくれます。ただ、やっかいなのは、すべてパブリックのプロパティだけで構成されていたら簡単、なのであって、そうじゃない場合は何が起こっているのか分からず苦しむ、なんてことにつながります。私がやってしまったのは、次のようなgetter/setterを持っているオブジェクトのデシリアライズがちゃんとできなくていつもデフォルト値が返ってしまう、というものです。
public class interval
{
public int cycle {get; set; }
}
[JsonObject]
public class Setting
{
private interval _interval = null;
const int DEFAULT_CYCLE = 10000; // 10秒間隔がデフォルト値ですよ
[JsonProperty]
public interval Interval
{
get {
interval ret;
if (_interval != null) {
ret = _interval;
}
else {
ret = new interval();
ret.cycle = DEFAULT_CYCLE;
}
return ret;
}
set {
_interval = value;
}
}
public static Setting Decode(string str)
{
return JsonConvert.DeserializeObject<Setting>(str);
}
}
上記のようなコードを書いたところ(正直、余裕だと思っていた)、strに
{
Interval : { cycle : 5000 } // 5秒間隔に書き換えるか
}
のようなJSON文字列を渡してもIntervalプロパティからはcycle : 10000のデフォルト値しか取れなくなっちゃったのですね。1時間ちょっと、なんでできないのか分からなかった(笑)。
NewtonJSONの挙動
このからくりは、次の事柄を十分把握してないとハマりますね。
- NewtonJSONは、デシリアライズする対象がオブジェクトであった場合、まずオブジェクトに対してgetterで値を取ってみて、何らかすでにオブジェクトが入っていたらそれを保持する。
- getterでオブジェクトが取れなかったらそこで初めてsetterを起動して、JSON文字列からオブジェクトを生成するのがデフォルトの挙動である。
ドキュメントをよく読めば上記の動作なんだな、ということは把握できるのですが、人間ものぐさというか、必要に迫られて初めて把握する生き物なので、私もご多分に漏れず把握しきれておらず、ハマっちゃった次第です。
つまり、先のプログラム例では、getterにてデフォルトに当たるオブジェクトを動的に生成しているのが問題で、これがgetterにあると、先にgetterで読み込んでオブジェクト参照があったらそれを保持するのがデフォルト挙動なので、結局setterは呼ばれることなく(せっかくJSONで指定したオプションが一切呼ばれず)スルーされる、という事になっちゃったわけです。
回避策
一度ハマればなんてことないのですが、このケースではデシリアライズする際にJsonSerializerSettingsを生成して、オブジェクトは上書きでお願いね、という指定をするととりあえず回避可能です。ドキュメントを漁るとちゃんと書いてあるんですけれどね。そこまで読まないで使っちゃうのが人間の性って奴で・・・。
public static Setting Decode(string str)
{
return JsonConvert.DeserializeObject<Setting>(str, new JsonSerializerSettings
{
ObjectCreationHandling = ObjectCreationHandling.Replace
});
}