3

Here is what I have got till now. Thanks to Brian Rodgers:

public class JsonSerializeTest
{
    [Fact]
    public void deserialize_test()
    {
        var settings = new JsonSerializerSettings { ContractResolver = new CustomContractResolver() };

        var jsonString = "{\"PropertyA\":\"Test\",\"PropertyB\":null}";
        var jsonObject = JsonConvert.DeserializeObject<NoConfigModel>(jsonString, settings);
        Assert.NotNull(jsonObject);
        
    }
}

public class NoConfigModel
{
    public string PropertyA { get; set; }
    public int PropertyB { get; set; }
    public bool PropertyC { get; set; }

}

class CustomContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        property.ShouldDeserialize = instance =>
        {
            try
            {
                PropertyInfo prop = (PropertyInfo)member;
                if (prop.CanRead)
                {
                    var value = prop.GetValue(instance, null);// getting default value(0) here instead of null for PropertyB
                    return value != null;
                }
            }
            catch
            {
            }
            return false;
        };
        return property;
    }
}

My Problem:

Need to set default value to Not Nullable fields instead of Exception or whole object being null. Having missing value is not a problem (gives default value by DefaultContractResolver), but when a not nullable value is explicitly set as null in json then this gives exception.

My code above is close but not close enough. I think I need to find a way to know that the value is actually null from json and set ShouldDeserialize =false for those cases.

Community
  • 1
  • 1
Ruchan
  • 3,124
  • 7
  • 36
  • 72

1 Answers1

3

What you want is that, during deserialization, when a null value is encountered for a non-nullable member, to set a default (non-null) value back in the containing object. This can be done by overriding DefaultContractResolver.CreateProperty as follows:

class CustomContractResolver : DefaultContractResolver
{
    class NullToDefaultValueProvider : ValueProviderDecorator
    {
        readonly object defaultValue;

        public NullToDefaultValueProvider(IValueProvider baseProvider, object defaultValue) : base(baseProvider)
        {
            this.defaultValue = defaultValue;
        }

        public override void SetValue(object target, object value)
        {
            base.SetValue(target, value ?? defaultValue);
        }
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        if (property != null && property.PropertyType.IsValueType && Nullable.GetUnderlyingType(property.PropertyType) == null && property.Writable)
        {
            var defaultValue = property.DefaultValue ?? Activator.CreateInstance(property.PropertyType);

            // When a null value is encountered in the JSON we want to set a default value in the class.
            property.PropertyType = typeof(Nullable<>).MakeGenericType(new[] { property.PropertyType });
            property.ValueProvider = new NullToDefaultValueProvider(property.ValueProvider, defaultValue);

            // Remember that the underlying property is actually not nullable so GetValue() will never return null.
            // Thus the below just overrides JsonSerializerSettings.NullValueHandling to force the value to be set
            // (to the default) even when null is encountered.
            property.NullValueHandling = NullValueHandling.Include;
        }
        return property;
    }

    // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
    // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
    // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
    // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
    // See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
    static CustomContractResolver instance;

    // Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
    static CustomContractResolver() { instance = new CustomContractResolver(); }

    public static CustomContractResolver Instance { get { return instance; } }

}

public abstract class ValueProviderDecorator : IValueProvider
{
    readonly IValueProvider baseProvider;

    public ValueProviderDecorator(IValueProvider baseProvider)
    {
        if (baseProvider == null)
            throw new ArgumentNullException();
        this.baseProvider = baseProvider;
    }

    public virtual object GetValue(object target) { return baseProvider.GetValue(target); }

    public virtual void SetValue(object target, object value) { baseProvider.SetValue(target, value); }
}

Notes:

Working sample .Net fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thank you for the detailed answer, but I think it has the issue where null is trying to be set to not nullable value and gives exception. – Ruchan Sep 12 '18 at 06:20
  • @Ruchan - can you give a [mcve], maybe as an edit to your question or a [gist](https://help.github.com/articles/creating-gists/)? I did test the contract resolver here: https://dotnetfiddle.net/jkvbn6 – dbc Sep 12 '18 at 06:33
  • @Ruchan - you commented out the line that changed `JsonProperty.PropertyType` to be nullable: `//property.PropertyType = typeof(Nullable<>).MakeGenericType(new[] { property.PropertyType });`. When I uncomment the line as shown in the answer, it works: https://dotnetfiddle.net/x4Huhz – dbc Sep 12 '18 at 06:55
  • hmm strange, yeah uncommenting works on fiddle, but doesn't work on my enviornment. Looking into it – Ruchan Sep 12 '18 at 07:00
  • @Ruchan - OK I think I may see something, give me a minute. – dbc Sep 12 '18 at 07:07
  • @Ruchan - answer updated. https://dotnetfiddle.net/ is using an old version of Json.NET which may have tripped me up. – dbc Sep 12 '18 at 07:23
  • thats a broken link. Also, your code and my changes from fiddle both dont work on my local environment. I am using 4.6.1 framework if that changes anything. Error `Error setting value to 'PropertyB' on 'NoConfigModel'` – Ruchan Sep 12 '18 at 07:25
  • @Ruchan - Does the code from my updated answer not work on your local environment? I changed `CreateProperty()` to make sure the default value is always allocated. (Incidentally dotnetfiddle.net always is using an obsolete version of Json.NET, see https://dotnetfiddle.uservoice.com/forums/228764--net-fiddle-ideas/suggestions/10782315-load-the-specified-version-of-json-net-rather-than for details.) – dbc Sep 12 '18 at 07:28