I have a bunch of Json files that I want to deserialize to immutable classes.
E.g. a Json file like this :
{
"Prop1": 5,
"Prop3": {
"NestedProp1": "something",
"NestedProp3": 42
}
}
Should be deserialized to classes like that:
public class Outer
{
public int Prop1 { get; }
public int Prop2 { get; }
public Inner Prop3 { get; }
public string Prop4 { get; }
public Outer(int prop1, int? prop2, Inner prop3, string? prop4)
{
Prop1 = prop1;
Prop2 = prop2 ?? GetSensibleRuntimeProp2Default();
Prop3 = prop3;
Prop4 = prop4 ?? GetSensibleRuntimeProp4Default();
}
...
}
public class Inner
{
public string NestedProp1 { get; }
public int NestedProp2 { get; }
public int NestedProp3 { get; }
public Inner(string nestedProp1, int? nestedProp2, int nestedProp3)
{
NestedProp1 = nestedProp1;
NestedProp2 = nestedProp2 ?? GetSensibleRuntimeNestedProp2Default();
NestedProp3 = nestedProp3;
}
...
}
As one can see, some constructor parameters are nullable (ref or value types), so that some default value can be injected in case the value is not specified in the Json file. However the matching properties in the classes are not nullable.
The problem is System.Text.Json deserialization requires that constructor parameters and properties have the exact same type so if I try this I get an exception stating this requirement which is indeed documented.
How would I be able to work around this limitation? Could I somehow inject code in the deserilization process to insert my own policy for deserializing objects (while letting the default process handle values and arrays)?
I am dealing with existing classes and Json and I am not allowed to make changes like adding a nullable property to the classes in order to match the constructor parameter. The thing actually worked using Newtonsoft.Json and I am asked to convert it to using System.Text.Json.
I wrote come code that uses reflection to deserialize the first level object in the Json file using the target class constructor that best matches the Json object properties. It looks like that:
public static object CreateInstance(Type targetType, JsonObject prototype, JsonSerializerOptions serializerOptions)
{
var constructors = GetEligibleConstructors(targetType);
var prototypePropertySet = prototype.Select(kvp => kvp.Key)
.ToImmutableHashSet(StringComparer.CurrentCultureIgnoreCase);
var bestMatch = FindBestConstructorMatch(prototypePropertySet, constructors);
if (bestMatch is null)
throw new NoSuitableConstructorFoundException($"COuld not find a suitable constructor to instanciate {targetType.FullName} from \"{prototype.ToJsonString()}\"");
var valuedParams = GetParameterValues(bestMatch, prototype, serializerOptions);
return bestMatch.Constructor.Invoke(bestMatch.Parameters.Select(p => valuedParams[p.Name]).ToArray());
}
(I'm deserializing from a JsonObject and not from text but I don't think it's relevant to the issue)
So basically:
- get all public constructors of the target type,
- find the one that has the most parameters in common with the Json object properties
- get the parameters values from the Json using standard System.Text.Json deserialization
- Invoke the constructor
Obviously this method limits me to the first level object since all the child properties will be handled by standard deserialization.
I would like to be able to do that recursively by using a similar code in a JsonConverter that would be called on deserializing Json objects while the deserialization of Json arrays or primitive values would be left to the standard converters.