0

I would like to run some code every time a specific class is being deserilized. (I cannot access//modify the class to add the [OnDeserialized] attributes)

My first approach was to use a custom jsonconverter and run some code in readjson. Thing is, the custom classes can be complex and working on deserializing all child properties sounds too much (when all gets deserialized properly without the converter).

Is there a way to add custom code whenever a type is deserialized? Or how to use the jsonconverter readjson to create and recursively populate its content (as the deserializer does without converters)?

Also one thing, the type is derived, so i cannot use the Jsonconverter.CanConvert method. (as it triggers on the base type)

This is just some ideas i used in readjson

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    // Load the JSON for the Result into a JObject
    JObject jObject = JObject.Load(reader);

    // Read the properties which will be used as constructor parameters
    //var enemy = Activator.CreateInstance(null, jObject["$type"]);

    object t = jObject["$type"];    
    string[] split = t.ToString().Split(',');
    string ass = split[1].Trim();
    string typ = split[0];

    var allassemblies = AppDomain.CurrentDomain.GetAssemblies();
    
    Assembly a = null;
    foreach (Assembly assembly in allassemblies)
    {
        if (assembly.FullName.StartsWith(ass))
        {
            a = assembly;
            break;
        }
    }

    Type tt = a.GetType(typ);

    var instance = Activator.CreateInstance(tt);
   
    //DO I REALLY NEED TO RECURSIVELY WORK THROUGH THE CONTENT (how to populate the instance?)

    //SOME CUSTOM CODE dependant on the deserialized type
    //before returning change working dir to the auc addons settings directory
    AddonUserControl auc = instance as AddonUserControl;
    dm_addon dma = AddonManager.FindAddonDataByAUCAssembly(auc);
    SystemExtensions.SetRelativeWorkingDirectory("settings//" + dma.Path);

    return instance;
}

Please note I cannot modify the type(s) being deserialized.

dbc
  • 104,963
  • 20
  • 228
  • 340
l2rek
  • 32
  • 1
  • 5
  • Can you modify the type in question? If so you can add an [`[OnDeserialized]`](https://www.newtonsoft.com/json/help/html/SerializationCallbacks.htm) callback to it, see e.g. [Deserializing such that a field is an empty list rather than null](https://stackoverflow.com/a/11946658/3744182). – dbc Jun 26 '20 at 13:43
  • See: [Json.NET serialization pre and post calls](https://stackoverflow.com/q/1048311/3744182). In fact I think this is a duplicate, agree? – dbc Jun 26 '20 at 15:19
  • By the way, your code is vulnerable to "Friday the 13th JSON Attacks" type injection attacks, see [TypeNameHandling caution in Newtonsoft Json](https://stackoverflow.com/q/39565954/3744182) for details. – dbc Jun 26 '20 at 17:08
  • Yes, `[OnDeserialized]` works correctly with `TypeNameHandling`, see https://dotnetfiddle.net/oLr3gh. Closing as a duplicate. Also, you could have used `serializer.Populate()` to populate your object, see [How can I populate an existing object from a JToken (using Newtonsoft.Json)?](https://stackoverflow.com/q/30220328/3744182). – dbc Jun 26 '20 at 17:15
  • Thanks! Wait : ) I cannot modify the type. – l2rek Jun 26 '20 at 17:40

1 Answers1

0

If you cannot modify the type to add an [OnDeserialized] callback, then the easiest way to ensure your custom logic is called for each AddonUserControl is to add the callback in runtime using a custom contract resolver.

First, define the following resolver:

public class ControlContractResolver : DefaultContractResolver
{
    static readonly SerializationCallback callback = (o, context) =>
        {
            if (o is AddonUserControl auc)
            {
                // Add your logic here:
                Debug.WriteLine("AddonUserControl's OnDeserializedCallback called for {0}", auc);
                dm_addon dma = AddonManager.FindAddonDataByAUCAssembly(auc);
                SystemExtensions.SetRelativeWorkingDirectory("settings//" + dma.Path);  
            }
        };
        
    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        if (typeof(AddonUserControl).IsAssignableFrom(objectType))
        {
            contract.OnDeserializedCallbacks.Add(callback);
        }
        return contract;
    }
}

Then allocate and cache a static instance somewhere, for performance:

static IContractResolver myResolver = new ControlContractResolver 
{
    // Set anything you might need here, e.g.
    //NamingStrategy = new CamelCaseNamingStrategy(),
}

And serialize as follows:

var settings = new JsonSerializerSettings
{
    ContractResolver = myResolver,
    TypeNameHandling = TypeNameHandling.Auto, // Or TypeNameHandling.Objects or etc.
    // Other settings as required,
};
var control2 = JsonConvert.DeserializeObject<BaseControl>(json, settings);

Be sure to use the same settings for serialization and deserialization.

Demo fiddle here.

If you still want to use a custom JsonConverter, you can populate your pre-allocated instance from the jObject by following the instructions shown in the answer to How can I populate an existing object from a JToken (using Newtonsoft.Json)?. Note, however, that your existing converter is vulnerable to "Friday the 13th JSON Attacks" such as the ones shown in TypeNameHandling caution in Newtonsoft Json. At the minimum you must check that the actual type is assignable to the expected type:

if (!typeof(BaseControlType).IsAssignableFrom(tt))
    throw new JsonSerializationException(string.Format("Unexpected type {0}", tt));

No matter which approach you choose, to reduce your chances of a successful attack you should consider creating a custom ISerializationBinder which sanitizes incoming types. If you use a custom contract resolver, the serialization binder will be invoked automatically. If you use a custom converter you will need to invoke the binder manually. To do that see e.g. this answer to How to deserialize json objects into specific subclasses?.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thank you for this awesome answer/information. I used serializer.populatevalues for now. BTW I needed this to store Imagebrushes which only have the image file name as source, not specifing the baseuri as it should be relative to the images (settings/theme) folder. I had to specify the working directory when the parent object was deserialized, so when the deserializer iterated through the child image brushes could find the files. (Directory.SetCurrentDirectory(..)) – l2rek Jun 28 '20 at 09:19
  • @l2rek - you're welcome. If this answers your question, please do [mark it as such](https://meta.stackexchange.com/q/5234). – dbc Jun 28 '20 at 14:24