Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent Json.NET from using use default values for missing constructor parameters while still using default values for properties? [closed]

Tags:

c#

.net

json.net

Is there a way to tell JSON.net that when it attempts to deserialize using a constructor (if there is no default constructor), that it should NOT assign default value to constructor parameters and that it should only call a constructor if every constructor parameter is represented in the JSON string? This same serializer SHOULD use default values when calling property/field setters, the rule is only scoped to constructors. None of the enum values here seem to be appropriate: http://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_DefaultValueHandling.htm

The solution should NOT rely on applying any attributes to the types being deserialized.

for example, the json string "{}" will deserialize to an object of type Dog by setting the Dog's age to 0 (the default value for an int). I'd like to a generalized, not-attribute-based solution to prevent this from happening. In this case, {"age":4} would work because age is specified in the JSON string and corresponds to the constructor parameter.

public class Dog
{
    public Dog(int age)
    {
        this.Age = age;
    }

    public int Age { get; }
}

However, if Dog is specified as such, then "{}" should deserialize to a Dog with Age == 0, because the Dog is not being created using a constructor.

public class Dog
{   
    public int Age { get; set; }
}

As to "why would you want to do this"... Objects with constructors are typically qualitatively different than POCOs as it relates to their properties. Using a constructor to store property values instead of settable properties on a POCO typically means that you want to validate/constrain the property values. So it's reasonable not to allow deserialization with default values in the presence of constructor(s).

like image 803
SFun28 Avatar asked May 24 '16 14:05

SFun28


1 Answers

When Json.NET encounters an object without a parameterless constructor but with a parameterized constructor, it will call that constructor to create the object, matching the JSON property names to the constructor arguments by name using reflection via a case-insensitive best match algorithm. I.e. a property whose name also appears in the constructor will be set via the constructor call, not the set method (even if there is one).

Thus, you can mark a constructor argument as required by marking the equivalent property as required:

public class Dog
{
    public Dog(int age)
    {
        this.Age = age;
    }

    [JsonProperty(Required = Required.Always)]
    public int Age { get; }
}

Now JsonConvert.DeserializeObject<Dog>(jsonString) will throw when the "age" property is missing.

Since this is something you always want, you can create a custom contract resolver inheriting from DefaultContractResolver or CamelCasePropertyNamesContractResolver that marks properties passed to the constructor as required automatically, without the need for attributes:

public class ConstructorParametersRequiredContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreatePropertyFromConstructorParameter(JsonProperty matchingMemberProperty, ParameterInfo parameterInfo)
    {
        var property = base.CreatePropertyFromConstructorParameter(matchingMemberProperty, parameterInfo);

        if (property != null && matchingMemberProperty != null)
        {
            var required = matchingMemberProperty.Required;
            // If the member is already explicitly marked with some Required attribute, don't override it.
            // In Json.NET 12.0.2 and later you can use matchingMemberProperty.IsRequiredSpecified to check to see if Required is explicitly specified.
            // if (!matchingMemberProperty.IsRequiredSpecified) 
            if (required == Required.Default)
            {
                if (matchingMemberProperty.PropertyType != null && (matchingMemberProperty.PropertyType.IsValueType && Nullable.GetUnderlyingType(matchingMemberProperty.PropertyType) == null))
                {
                    required = Required.Always;
                }
                else
                {
                    required = Required.AllowNull;
                }
                // It turns out to be necessary to mark the original matchingMemberProperty as required.
                property.Required = matchingMemberProperty.Required = required;
            }
        }

        return property;
    }
}

Then construct an instance of the resolver:

static IContractResolver requiredResolver = new ConstructorParametersRequiredContractResolver();

And use it as follows:

var settings = new JsonSerializerSettings { ContractResolver = requiredResolver };
JsonConvert.DeserializeObject<T>(jsonString, settings)

Now deserialization will throw if the "age" property is missing from the JSON.

Notes:

  • This only works if there is a corresponding property. There doesn't appear to be a straightforward way to mark a constructor parameter with no corresponding property as required.

  • Newtonsoft recommends that you cache and reuse the contract resolver for best performance.

Demo fiddle here.

like image 162
dbc Avatar answered Sep 19 '22 14:09

dbc