Search code examples
c#json.net

Newtonsoft.Json - how can I choose the constructor to deserialize based on passed object properties types?


I have a generic type with 2 constructor each accepting 1 parameter with different types.

There is no problem with serialization. But when I try to deserialize, I get an error that Newtonsoft is Unable to find a constructor to use for this type.

How can I override Newtonsoft's default behavious and choose the constructor to be used for deserialization based on the property types?

I DO NOT have access to the Test class, so I can not change it in any way. I wish to use another way not write a an entirely custom JsonConverter.

I saw this answer here that you can override the DefaultContractResolver. It works well to choose a different constructor, but I can not see how I can access the properties of the object I am trying to deserialize?

How can I choose a constructor based on the properties types of the object to be deserialized?

public class Test<T, U> where U : struct
{
    public Test(T firstProperty)
    {
        FirstProperty = firstProperty;
    }

    public Test(U secondProperty)
    {
        SecondProperty = secondProperty;
    }

    public T FirstProperty { get; }

    public U SecondProperty { get; }
}

Solution

  • There is no way to configure Json.NET to choose a constructor based on the presence or absence of certain properties in the JSON to be deserialized. It simply isn't implemented.

    As a workaround, you can create a custom JsonConverter<Test<T, U>> that deserializes to some intermediate DTO that tracks the presence of both properties, and then chooses the correct constructor afterwards. Then you can create a custom contract resolver that applies the converter to all concrete types Test<T, U>.

    The following converter and contract resolver perform this task:

    class TestConverter<T, U> : JsonConverter where U : struct
    {
        // Here we make use of the {PropertyName}Specified pattern to track which properties actually got deserialized.
        // https://stackoverflow.com/questions/39223335/how-to-force-newtonsoft-json-to-serialize-all-properties-strange-behavior-with/
        class TestDTO
        {
            public T FirstProperty { get; set; }
            [JsonIgnore] public bool FirstPropertySpecified { get; set; }
            public U SecondProperty { get; set; }
            [JsonIgnore] public bool SecondPropertySpecified { get; set; }
        }
        
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var dto = serializer.Deserialize<TestDTO>(reader);
            if (dto == null)
                return null;
            else if (dto.FirstPropertySpecified && !dto.SecondPropertySpecified)
                return new Test<T, U>(dto.FirstProperty);
            else if (!dto.FirstPropertySpecified && dto.SecondPropertySpecified)
                return new Test<T, U>(dto.SecondProperty);
            else
                throw new InvalidOperationException(string.Format("Wrong number of properties specified for {0}", objectType));
        }
    
        public override bool CanConvert(Type objectType) => objectType == typeof(Test<T, U>);
        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
    }
    
    public class TestContractResolver : DefaultContractResolver
    {
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            var contract = base.CreateObjectContract(objectType);
            if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Test<,>))
                contract.Converter = (JsonConverter)Activator.CreateInstance(typeof(TestConverter<,>).MakeGenericType(objectType.GetGenericArguments()));
            return contract;
        }
    }
    

    Then use them e.g. as follows:

    var json1 = @"{""FirstProperty"":""hello""}";
    var json2 = @"{""SecondProperty"": 10101}";
    
    IContractResolver resolver = new TestContractResolver(); // Cache this statically for best performance
    var settings = new JsonSerializerSettings
    {
        ContractResolver = resolver,
    };
    var test1 = JsonConvert.DeserializeObject<Test<string, long>>(json1, settings);
    Assert.AreEqual(test1.FirstProperty, "hello");
    var test2 = JsonConvert.DeserializeObject<Test<string, long>>(json2, settings);
    Assert.AreEqual(test2.SecondProperty, 10101L);
    

    Notes:

    • The converter throws an InvalidOperationException if the JSON does not contain exactly one of the two properties.

      Feel free to modify this as per your requirements.

    • The converter does not implement serialization as your Test<T, U> type does not provide a method to track which property was initialized.

    • The converter does not attempt to handle subclasses of Test<T, U>.

    Demo fiddle here.