Search code examples
c#json.netjson-deserializationvalue-provider

How to customize value setting during deserialization with contract resolver and value provider


I am failing at a task to encrypt certain fields when serializing into JSON and decrypt those fields during deserialization into a specific C# class.

I reduced the problem to its most basic issue, which is that I cannot customize the deserialization of specific fields by manipulating the value, and I don't know the reason. I am using a custom contract resolver and a custom value provider for each field. I can see that the GetValue function is executed but the SetValue is never executed.

The code sample:

class Program
{
    static void Main(string[] args)
    {

        var text = "This is text";
        var number = 1;
        var anotherText = "This is another text";
        var anotherNumber = 2;
        var sampleInner = new SampleInner(anotherText, anotherNumber);
        var sample = new SampleMessage(text, number, sampleInner);

        var myCustomContractResolver = new MyCustomContractResolver();
        var jsonSettings = GetJsonSettings(myCustomContractResolver);

        Console.WriteLine("Serializing..");
        var json = JsonConvert.SerializeObject(sample, jsonSettings);
        Console.WriteLine(json);

        Console.WriteLine("Deserializing..");
        var sampleDeserialized = JsonConvert.DeserializeObject(json, typeof(SampleMessage), jsonSettings);
        Console.WriteLine(sampleDeserialized);

        Console.ReadLine();
    }

    private static JsonSerializerSettings GetJsonSettings(IContractResolver contractResolver)
    {
        var jsonSettings =
            new JsonSerializerSettings
            {
                ContractResolver = contractResolver
            };
        return jsonSettings;
    }
}

the custom contract resolver:

public class MyCustomContractResolver
    : DefaultContractResolver
{
    public MyCustomContractResolver()
    {
        NamingStrategy = new CamelCaseNamingStrategy();
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var jsonProperties = base.CreateProperties(type, memberSerialization);

        foreach (var jsonProperty in jsonProperties)
        {
            var propertyInfo = type.GetProperty(jsonProperty.UnderlyingName);
            var defaultValueProvider = jsonProperty.ValueProvider;
            jsonProperty.ValueProvider = new MyValueProvider(defaultValueProvider);
        }

        return jsonProperties;
    }
}

and the custom value provider whose SetValue is never executed during deserialization:

public class MyValueProvider
    : IValueProvider
{
    private readonly IValueProvider _valueProvider;

    public MyValueProvider(IValueProvider valueProvider)
    {
        _valueProvider = valueProvider;
    }

    public void SetValue(object target, object value)
    {
        //This is not executed during deserialization. Why?
        _valueProvider.SetValue(target, value);
        Console.WriteLine($"Value set: {value}");
    }

    public object GetValue(object target)
    {
        var value = _valueProvider.GetValue(target);
        Console.WriteLine($"Value get: {value}");
        return value;
    }
}

Here is the sample code to reproduce it in case it's needed.

Hopefully somebody can let me know what am I missing :)

UPDATE 1: The object I serialize/deserialize is immutable (no public setters) and that's a requirement because I need to support such objects. As a comment points out, then it makes sense there is no SetValue executed

UPDATE 2: Thanks to @dbc for the brilliant answer no I know a good workaround for deserialization into immutable object. The final version code after the accepted answer.

UPDATE 3: The chosen answer is absolutely correct given the question. However, after investigating further I decided to go with a slightly different approach that works for both immutable and mutable classes in case somebody is in a similar situation. Instead using value providers I use now a combination of contract resolver and json converter so that with the contract resolver I can decide how to serialize/deserialize based on the type, and with the json converter I can access the value during serialization/deserialization and manipulate as desired.

Basically, on my contract resolver I override the method to create properties (where I can access my original Type properties) and selectively I specify which json converter to use.

protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
    var jsonProperties = base.CreateProperties(type, memberSerialization);

    //Filter here based on type, attribute or whatever and if want to customize a specific property type:
    foreach (var jsonProperty in jsonProperties)
    {
        jsonProperty.Converter = new MyJsonConverter();
    }

    return jsonProperties;
}

and in the MyJsonConverter we can choose what to do when writing into json or when reading from json:

public class MyJsonConverter
    : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //I can do whatever with value
        var finalValue = $"{value}-edited";
        writer.WriteValue(finalValue);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // I can do whatever with the value
        var value = (string)reader.Value;
        var newValue = "whatever";
        return newValue;
    }

    public override bool CanWrite => true;

    public override bool CanRead => true;

    public override bool CanConvert(Type objectType)
    {
        return true;
    }
}

Solution

  • The reason that IValueProvider.SetValue is not called for the properties of SampleInner is that SampleInner is immutable and so there are no set methods to be called. Instead, the JSON properties are matched to arguments of the type's single parameterized constructor by name (modulo case), deserialized to the type of the matched argument, and then passed into the constructor as explained here.

    Even if you were to make the properties mutable the setter will not be called for properties already passed into the constructor, as Json.NET makes the (reasonable) assumption that passing a property value into the constructor is sufficient to set the property's value.

    So, what are your options?

    Firstly, you could make your types mutable with a default constructor. The setters and constructor could be private as long as they are marked with appropriate attributes:

    public class SampleInner
    {
        [JsonProperty] // Adding this attribute informs Json.NET that the private setter can be called.
        public string AnotherText { get; private set; }
    
        [JsonProperty]
        public int AnotherNumber { get; private set; }
    
        [JsonConstructor] // Adding this attribute informs Json.NET that this private constructor can be called
        private SampleInner() { }
    
        public SampleInner(string anotherText, int anotherNumber)
        {
            this.AnotherText = anotherText;
            this.AnotherNumber = anotherNumber;
        }       
    }
    

    Now that there are setters to be called, your MyValueProvider.SetValue() will get invoked. Demo fiddle #1 here.

    Secondly, if you cannot so modify your types, you could wrap the constructor method called in some decorator that does the necessary pre-processing, however this is made difficult by the fact that JsonObjectContract.ParameterizedCreator is nonpublic. Thus you cannot get direct access to the parameterized constructor that Json.NET has chosen, in order to decorate it. You can, however, determine its arguments, which are specified by JsonObjectContract.CreatorParameters. When this collection is populated then either OverrideCreator is set or the (secret) ParameterizedCreator is set. This allows the necessary logic to be inserted as follows:

    public class MyCustomContractResolver : DefaultContractResolver
    {
        public MyCustomContractResolver() { NamingStrategy = new CamelCaseNamingStrategy(); }
    
        static ObjectConstructor<Object> GetParameterizedConstructor(JsonObjectContract contract)
        {
            if (contract.OverrideCreator != null)
                return contract.OverrideCreator;
    
            // Here we assume that JsonSerializerSettings.ConstructorHandling == ConstructorHandling.Default
            // If you would prefer AllowNonPublicDefaultConstructor then you need to remove the check on contract.DefaultCreatorNonPublic
            if (contract.CreatorParameters.Count > 0 && (contract.DefaultCreator == null || contract.DefaultCreatorNonPublic))
            {
                // OK, Json.NET has a parameterized constructor stashed away in JsonObjectContract.ParameterizedCreator
                // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonObjectContract.cs#L100
                // But, annoyingly, this value is internal so we cannot get it!
                // But because CreatorParameters.Count > 0 and OverrideCreator == null we can infer that such a constructor exists, and so call it using Activator.CreateInstance
    
                return (args) => Activator.CreateInstance(contract.CreatedType, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, args, CultureInfo.InvariantCulture);
            }
    
            return null;
        }
    
        static ObjectConstructor<Object> CustomizeConstructor(JsonObjectContract contract, ObjectConstructor<Object> constructor)
        {
            if (constructor == null)
                return null;
            return (args) =>
            {
                // Add here your customization logic.
                // You can match creator parameters to properties by property name if needed.
                foreach (var pair in args.Zip(contract.CreatorParameters, (a, p) => new { Value = a, Parameter = p }))
                {
                    // Get the corresponding property in case you need to, e.g., check its attributes:
                    var property = contract.Properties[pair.Parameter.PropertyName];
    
                    if (property == null)
                        Console.WriteLine("Argument {0}: Value {1}", pair.Parameter.PropertyName, pair.Value);
                    else
                        Console.WriteLine("Argument {0} (corresponding to JsonProperty {1}): Value {2}", pair.Parameter.PropertyName, property, pair.Value);
                }
                return constructor(args);
            };
        }
    
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            var contract = base.CreateObjectContract(objectType);
    
            contract.OverrideCreator = CustomizeConstructor(contract, GetParameterizedConstructor(contract));
    
            return contract;
        }
    
        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            var jsonProperties = base.CreateProperties(type, memberSerialization);
    
            foreach (var jsonProperty in jsonProperties)
            {
                var defaultValueProvider = jsonProperty.ValueProvider;
                jsonProperty.ValueProvider = new MyValueProvider(defaultValueProvider);
            }
    
            return jsonProperties;
        }
    }
    

    Notes:

    • If a default constructor exists but is nonpublic, the above contract resolver assumes that it is not used. If you would prefer nonpublic default constructors to be used, you will need to set JsonSerializerSettings.ConstructorHandling == ConstructorHandling.AllowNonPublicDefaultConstructor and also modify the code in GetParameterizedConstructor() above to remove the check on contract.DefaultCreatorNonPublic:

          if (contract.CreatorParameters.Count > 0 && contract.DefaultCreator == null)
      
    • It would be reasonable to request an enhancement to allow access to, and customization of, JsonObjectContract.ParameterizedCreator.

      (I suppose you could try to access JsonObjectContract.ParameterizedCreator directly via reflection...)

    Demo fiddle #2 here.