In a real-world situation I know in advance that a particular API endpoint among many will (perhaps temporarily) produce JSON with a list of known minor errors or variations and I want to pass run-time parameters to the JSON de-serializer to allow for this. I am happy to write a JsonConverter
for each type to handle variations but am wondering on the best (also most performant) way to pass information to specify the corrections to the de-serialization/JsonConverter at run-time?
I do not want the variations to be detected dynamically inside the JsonConverter
as I want to maintain a list of which endpoints have what variations and update this list as things change. We are talking here about a situation where there are dozens of endpoint providers (with support desks) and so I want to raise issues with them in parallel to working around issues in my code.
I do not want to create multiple target classes or JsonConverter
s for each error variant as the target type is usually complex and the errors are mostly small issues (e.g. Date-time format errors, use of string array instead of space-separated values) which may occur in any combination. I believe corrections are best handled by some kind of message-passing to JsonConverter
s which can handle the variations.
Here is a simplified example involving claims of a received JSON token. Depending on the provider I want to send a parameter to both the DateTimeOffsetUnixConverter
and DateTimeOffsetUnixConverter
converters instructing the desired deserialization behaviour. I've included the code for DateTimeOffsetUnixConverter
where I've added a property MilliSecondsNotSeconds
to allow for two de-serialisation options.
namespace MyNamespace
{
public class ClaimsSubset
{
// Unix epoch integer
// Sometimes seconds, other times milli-seconds
[JsonConverter(typeof(DateTimeOffsetUnixConverter))]
[JsonProperty("iat")]
public DateTimeOffset Iat { get; set; }
// Sometimes this is string with space-separated values, sometimes string array
[JsonProperty("scope")]
[JsonConverter(typeof(StringArrayConverter))]
public string Scope { get; set; }
}
public class DateTimeOffsetUnixConverter : JsonConverter<DateTimeOffset>
{
public bool MilliSecondsNotSeconds { get; set; } = false;
public override void WriteJson(JsonWriter writer, DateTimeOffset value, JsonSerializer serializer)
{
long seconds = value.ToUnixTimeSeconds();
long timeValue = MilliSecondsNotSeconds ? (seconds * 1000) : seconds;
JToken jt = JToken.FromObject(seconds);
jt.WriteTo(writer);
}
public override DateTimeOffset ReadJson(
JsonReader reader,
Type objectType,
DateTimeOffset existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
if (objectType == typeof(DateTimeOffset))
{
long timeValue = long.Parse(reader.Value.ToString());
long seconds = MilliSecondsNotSeconds ? (timeValue / 1000) : timeValue;
return DateTimeOffset.FromUnixTimeSeconds(seconds);
}
throw new NotSupportedException($"The type {objectType} is not supported.");
}
}
}
Following a discussion in the comments I tried to use code like the following to configure DateTimeOffsetUnixConverter
at the de-serialisation call site (where endpoint providers are known). The code below doesn't work however as Json.Net does not seem to use the provided DateTimeOffsetUnixConverter
when de-serialising the properties of ClaimsSubset
.
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings
.Converters
.Add(
new DateTimeOffsetUnixConverter
{
MilliSecondsNotSeconds = true
});
JsonConvert.DeserializeObject<ClaimsSubset>(json, jsonSerializerSettings)
Any advice much appreciated!