2

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;
    }
}
diegosasw
  • 13,734
  • 16
  • 95
  • 159
  • Where is the definition of `SampleInner`? Perhaps it has a parameterized constructor? – dbc Jul 14 '19 at 19:54
  • Yes it does: https://gitlab.com/DiegoDrivenDesign/DiDrDe.CustomJsonSerialization/blob/e4812721d8c4e1d1ce32a1c50c8d003868350fb4/DiDrDe.CustomJsonSerialization/Messages/SampleInner.cs – diegosasw Jul 15 '19 at 05:12
  • But newtonsoft supports that. There's no way of setting a value with value resolver if the class is immutable? – diegosasw Jul 15 '19 at 05:14
  • You might want to edit your question to include that, I had to hunt around a while to find it (it was under the sub-directory Messages). – dbc Jul 15 '19 at 05:28
  • 1
    Well of course not -- if the type is immutable then there is no set method to be called, thus no way to wrap the set method in some logic. I'll write up an answer with some alternatives. – dbc Jul 15 '19 at 05:43
  • You might want to extract **UPDATE 3** into a separate [self-answer](https://stackoverflow.com/help/self-answer) rather than leaving it in the question. It's a valid solution to the problem that differs from mine. – dbc Jul 19 '19 at 21:44
  • I thought about it, but the question was about value provider, so technically I'd have to edit my question also or my answer would not really stick to it don't you think @dbc? – diegosasw Jul 19 '19 at 22:32
  • 1
    Even if there is already an "accepted" canonical answer, there's nothing wrong with an additional answer that offers an alternative approach. – dbc Jul 19 '19 at 23:33

1 Answers1

3

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • This is seriously a great answer, at par with the best I have seen on SO, thanks! Could you please hint how/where/when we could optimize the approach – Srivathsa Harish Venkataramana Aug 14 '23 at 20:11
  • @SrivathsaHarishVenkataramana - the performance problem with this solution is that `Activator.CreateInstance()` is slow, but there's no equivalent to `Delegate.CreateDeletate()` for `ConstructorInfo`. Instead you will need to do something trickier, see e.g. [Activator.CreateInstance Performance Alternative](https://stackoverflow.com/q/4432026). – dbc Aug 14 '23 at 20:24
  • @SrivathsaHarishVenkataramana - Or you could look at Newtonsoft's various implementations of [`ReflectionDelegateFactory.CreateParameterizedConstructor()`](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/ReflectionDelegateFactory.cs#L76). Json.NET uses [different logic](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs#L517) for manufacturing delegates from constructors depending on the current permissions. – dbc Aug 14 '23 at 20:25