【C#】JSON.NETで抽象化したシリアライズのコールバックを扱えない


はじめに

タイトルの通り、Json.NETではシリアライズのコールバックで抽象を扱えません。本記事での抽象は、virtual /interfaceを指しています。
検証時のバージョンにおいてAPI仕様には記載はありませんが、ソースコード上で抽象を例外と扱うことを確認しました。また、StackOverflowで類似事象を確認しました。
本記事に調査方法、実行結果、調査結果を記載します。

前置き

Json.NETはシリアライズ時のコールバックをサポートしている。
引用 Serialization Callbacks

Json.NET supports serialization callback methods. A callback can be used to manipulate an object before and after its serialization and deserialization by the JsonSerializer.より

MS DOC: OnDeserializedAttributeより引用

OnDeserializedAttributeを使用するには、メソッドにStreamingContextパラメーターが含まれている必要があります。

[OnDeserialized]
private void SetValuesOnDeserialized(StreamingContext context)
{
    // Code not shown.
}

ドキュメントには、特に抽象化に関する注意書きはない。

方法

実装

  • インターフェース、デシリアライズに使用するデータ型を定義する。
インターフェース
public interface INewtonCallBackAttribute
{
    string Message { get; set; }
    void CallBackMethod(StreamingContext context);
}
デシリアライズに使用するデータ型
public class NewtonCallBackAttribute : INewtonCallBackAttribute
{
    public string Message { get; set; }

    [OnDeserialized]
    public void CallBackMethod(StreamingContext context)
    {
        Message = "Deserialized";
    }
}
  • デシリアライズを実行する。
メイン処理
internal class Program
{
    public static void Main(string[] args)
    {
        try
        {
            var jsonString = @"{""Message"":""Initialize""}" ;
            Console.WriteLine(jsonString);
            var deserialize = JsonConvert.DeserializeObject<NewtonCallBackAttribute>(jsonString);
            Console.WriteLine(deserialize?.Message);
        }
        catch (JsonException ex)
        {
            Console.WriteLine(ex);
        }
    }
}

検証環境

.NET Framework 4.6.2
JSON.NET(Newtonsoft.Json ) 13.0.1

実行結果

実行時の例外

仮想メソッドを[System.Runtime.Serialization.OnDeserializedAttribute]でマークできないって言ってる。

抜粋

Newtonsoft.Json.JsonException: Virtual Method 'Void CallBackMethod(System.Runtime.Serialization.StreamingContext)' of type 'JsonCallback.NewtonCallBackAttribute' cannot be marked with
'System.Runtime.Serialization.OnDeserializedAttribute' attribute.

例外の詳細
Newtonsoft.Json.JsonException: Virtual Method 'Void CallBackMethod(System.Runtime.Serialization.StreamingContext)' of type 'JsonCallback.NewtonCallBackAttribute' cannot be marked with
'System.Runtime.Serialization.OnDeserializedAttribute' attribute.
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.IsValidCallback(MethodInfo method, ParameterInfo[] parameters, Type attributeType, MethodInfo currentCallback, Type& prevA
ttributeType)
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.GetCallbackMethodsForType(Type type, List`1& onSerializing, List`1& onSerialized, List`1& onDeserializing, List`1& onDeser
ialized, List`1& onError)
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.ResolveCallbackMethods(JsonContract contract, Type t)
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.InitializeContract(JsonContract contract)
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.CreateObjectContract(Type objectType)
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.CreateContract(Type objectType)
   場所 System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   場所 Newtonsoft.Json.Serialization.DefaultContractResolver.ResolveContract(Type type)
   場所 Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   場所 Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   場所 Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   場所 Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   場所 Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value)
   場所 JsonCallback.Program.Main(String[] args) 場所 C:\Users\1125s\source\repos\SY81517\Quiita\SerializeRuntimeTest\JsonCallback\Program.cs:行 14

試しにインターフェースにも属性の指定を付けたが、同様の実行例外が発生した。

インターフェース
public interface INewtonCallBackAttribute
{
    string Message { get; set; }
    [OnDeserialized] 
    void CallBackMethod(StreamingContext context);
}

解決方法
下記のコールバックの関数定義をインターフェースから除外することで解消する。

インターフェース
public interface INewtonCallBackAttribute
{
    string Message { get; set; }
}

例外の発生原因の調査結果

JSON.NETの調査

API仕様書

API仕様を確認したが、抽象に関するヒントはない。
DefaultContractResolver

ソース調査

例外情報のスタックトレースから、処理を追った。
例外の発生箇所 DefaultContractResolver.csのIsValidCallback()内部で仮想メソッドを「異常」と扱う処理を確認した。
Newtonsoft.Json.Serialization.DefaultContractResolver.cs 1298行目

ソース抜粋。

DefaultContractResolver.cs 1317行目
private static bool IsValidCallback(MethodInfo method, ParameterInfo[] parameters, Type attributeType, MethodInfo currentCallback, ref Type prevAttributeType)
{
    //省略
            if (method.IsVirtual)
            {
                throw new JsonException("Virtual Method '{0}' of type '{1}' cannot be marked with '{2}' attribute.".FormatWith(CultureInfo.InvariantCulture, method, GetClrTypeFullName(method.DeclaringType), attributeType));
            }
    //省略
}

StackOver flowで類似調査

類似を発見した。
Why can't serialization callback methods be virtual?

An unhandled exception of type 'System.TypeLoadException'.... Type 'BaseObj' in assembly ... has method 'OnDeserialization' which is either static, virtual, abstract or generic, but is marked as being a serialization callback method.

おそらくJSON.NETのバージョンが古いため、例外の発生内容が異なる。
注目したやり取りは下記二件。.NETランタイムが型安全のために制限を掛けているという議論である。
抽象化したコールバックできないのは、この議論にヒントがありそう!

やり取り1

A mere guess: The method will be called via reflection based upon its MethodInfo. Whichever code is used to discover the MethodInfo in the first place might, for some reason, not discover the most specialized version of the method, but also does not go through the trouble of looking for it oneself. When invoking the method via its MethodInfo, the call will not be evaluated in a polymorphic way, which may result in unexpected behaviour. – O. R. Mapper Jul 19, 2017 at 14:40

機械翻訳
単なる推測ですが、このメソッドはMethodInfoに基づいてリフレクションで呼び出されるでしょう。最初にMethodInfoを発見するために使われるどのコードも、何らかの理由でメソッドの最も特化したバージョンを発見しないかもしれませんし、自分自身でそれを探す手間をかけることもありません。MethodInfo を介してメソッドを呼び出す場合、呼び出しは多相的な方法で評価されないので、予期しない動作になることがあります。-
O. R.マッパー 2017年7月19日 14:40

やり取り2

Firstly, it's not Json.NET throwing the exception; the .Net runtime itself is throwing the exception from the static constructor for BaseObj, which indicates that the type itself is invalid. As to why this is, I can only guess it's for security reasons: calls to a a virtual serialization callback for a fully trusted type could be overridden blocked by a partially trusted derived type. This is, I believe, why ISerializable is not available in partial trust: derived types could examine or modify the base type serialization process. – dbc Jul 19, 2017 at 20:51

機械翻訳
まず、Json.NETが例外を投げているのではなく、.Netランタイム自体がBaseObjの静的コンストラクタから例外を投げており、これは型自体が無効であることを表しているのです。完全に信頼できる型の仮想シリアライゼーション コールバックへの呼び出しが、部分的に信頼できる派生型によってオーバーライド ブロックされる可能性があるためです。これは、ISerializableが部分的に信頼された状態で利用できない理由だと思います。派生型は基本型のシリアライズ処理を調べたり、変更したりすることができます。

.NET Frameworkの調査

.NET Frameworkの内部ソース調査
特に、仮想化メソッドに関するコメントはなし。抽象化すること自体、想定されてないのか?
.NET内部ソース