The solution was easier than expected. You can register a JsonConverter
with GlobalConfiguration
. I made an abstract wrapper around JsonConverter
as below, the idea for which came from the following SO thread: How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?.
public abstract class CustomJsonConverter<T, TResult> : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load token from stream
var token = JToken.Load(reader);
// Create target object based on token
var target = Create(objectType, token);
var targetType = target.GetType();
if (targetType.IsClass && targetType != typeof(string))
{
// Populate the object properties
var tokenReader = token.CreateReader();
CopySerializerSettings(serializer, tokenReader);
serializer.Populate(token.CreateReader(), target);
}
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
T typedValued = (T)value;
TResult valueToSerialize = Convert(typedValued);
serializer.Serialize(writer, valueToSerialize);
}
public override bool CanConvert(Type objectType)
{
return typeof (T) == objectType;
}
protected virtual T Create(Type type, JObject jObject)
{
// reads the token as an object type
if (typeof(TResult).IsClass && typeof(T) != typeof(string))
{
return Convert(token.ToObject<TResult>());
}
var simpleValue = jObject.Value<TResult>();
return Convert(simpleValue);
}
protected abstract TResult Convert(T value);
protected abstract T Convert(TResult value);
private static void CopySerializerSettings(JsonSerializer serializer, JsonReader reader)
{
reader.Culture = serializer.Culture;
reader.DateFormatString = serializer.DateFormatString;
reader.DateTimeZoneHandling = serializer.DateTimeZoneHandling;
reader.DateParseHandling = serializer.DateParseHandling;
reader.FloatParseHandling = serializer.FloatParseHandling;
}
}
You can then use this to do something like the following
public class DateTimeToStringJsonConverter : CustomJsonConverter<DateTime, string>
{
protected override string Convert(DateTime value)
{
return value.ToString();
}
protected override DateTime Convert(string value)
{
return DateTime.Parse(value);
}
}
And then finally register an instance with GlobalConfiguration
in Global.asax.cs
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new DateTimeToStringJsonConverter());
Any thoughts/ideas/opinions? Thanks!
EDIT: 2018.10.16 - Updated code to handle value types as the source type and to copy serializer settings over thanks to the following comment from the original SO answer:
NOTE: This solution is all over the internet, but has a flaw that manifests itself in rare occasions. The new JsonReader created in the ReadJson method does not inherit any of the original reader's configuration values (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, etc...). These values should be copied over before using the new JsonReader in serializer.Populate().