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.