Introduction
備忘録。
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter を使った実装を Newtonsoft.Json (JSON.NET) に変更したプログラムを評価することがあった。
その際、逆シリアル化したオブジェクトの結果が変更前後で異なる事象に遭遇した。
問題の特定にかなりの時間を要したので残しておく。
What is happened?
実験コードは https://github.com/takuya-takeuchi/Demo/tree/master/Misc/29_JsonAndBinaryDeserialization に公開済み。
シリアル化・逆シリアル化する対象のクラスの実装がかなり特殊なのもあるが、下記のようなクラス。
1 | [ ] |
このクラスを
- Newtonsoft.Json
- System.Text.Json
- System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
でシリアル化し、逆シリアル化で復元すると結果が異なってしまった。
オブジェクトはこのように設定。
1 | var obj = new Sample(); |
逆シリアル化すると、 Newtonsoft.Json で逆シリアル化した時のみ、List.Count
が 2
になるのである。
調べると、Newtonsoft.Json は逆シリアル化の際、コンストラクタを使用して復元するのがデフォルトの挙動になっているのだが、コンストラクタが複数ある時、最も引数の多いコンストラクタが優先して使用されるという仕様になっているとのこと。
KeyValuePair<string, string>
を引数に持つコンストラクタが選ばれたとして、 List
の要素が余計に増えたのかはこれだけではわからない (List 同士が結合された?) が、こうした仕様の違いが、想定外の結果を生み出してしまった。
単純に System.Text.Json を使えば良かった、という話になりそうだが、プログラムが .NET Framework 4.8 で動いており、.NET に移行はできなかった。
何より、System.Runtime.Serialization.Formatters.Binary.BinaryFormatter の実装も共存する必要があったため、.NET への移植も難しい。
(.NET 7 でBinaryFormatter は廃止されてしまった 参考:SerializationFormat.Binary は廃止されました)
というわけで苦労した結果、下記のように Newtonsoft.Json の逆シリアル化の挙動を変更できることが分かった。
1 | internal sealed class UninitializedObjectResolver : DefaultContractResolver |
System.Runtime.Serialization.FormatterServices.GetUninitializedObject
メソッドを使うことで、コンストラクタを呼び出さずにオブジェクトを生成するという回避策。
このおかげで List.Count
が 1
になり、変更前後で挙動が一致するようになった。
Source Code
https://github.com/takuya-takeuchi/Demo/tree/master/Misc/29_JsonAndBinaryDeserialization