2

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; }
}
MiBuena
  • 451
  • 3
  • 22
  • Does this answer your question? [JSON.net: how to deserialize without using the default constructor?](https://stackoverflow.com/questions/23017716/json-net-how-to-deserialize-without-using-the-default-constructor) – NavidM Jun 07 '22 at 20:09
  • You will have to write a custom converter, because you will need to preload the JSON into a `JObject`, check which properties are present, and decide which constructor to call based on that. I don't see how Json.NET could do that automatically since your JSON could contain both values and you will need to decide which to discard. – dbc Jun 07 '22 at 23:09
  • @NavidM - no that does not answer the question. The constructor to be used can only be chosen in runtime, based on the actual properties present in the JSON, rather that at compile time, so applying `[JsonConstructor]` will not work. – dbc Jun 07 '22 at 23:10

1 Answers1

0

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.

dbc
  • 104,963
  • 20
  • 228
  • 340