Reputation: 1876
I want to layer multiple JSON converters for one property because I need to specify a converter for the property's inner type that is generic.
Use case:
public class A {
public Option<DateTime> Time { get; set; }
}
I've got an OptionJsonConverter
that can deserialize any Option
and I want to specify a custom date format string for this property using the DateFormatConverter
from this answer.
Writing a custom converter would be a solution, but it's not ideal as I'll have huge code duplication.
I could use a nullable type, but I've already committed my code base to Option
s in an effort to avoid null comparisons, and this issue may arise for other types in the future anyway.
Upvotes: 1
Views: 1209
Reputation: 1876
Converters can modify serializer's Converters
property during ReadJson
and WriteJson
invocation and the new collection's contents are honored during nested serializations and deserializations.
With this, we can make a converter that temporarily adds specified converters to the Converters
property like so:
public abstract class CascadeJsonConverterBase : JsonConverter
{
private readonly JsonConverter[] augmentConverters;
protected CascadeJsonConverterBase() : this(new JsonConverter[0]) { }
// this constructor is intended for use with JsonConverterAttribute
protected CascadeJsonConverterBase(object[] augmentConverters)
: this(augmentConverters.Select(FromAttributeData).ToArray())
{ }
protected CascadeJsonConverterBase(JsonConverter[] augmentConverters)
{
this.augmentConverters = augmentConverters;
}
protected static JsonConverter FromAttributeData(object augmentConverterObj)
{
if (!(augmentConverterObj is object[] augmentConverter))
{
throw new ArgumentException($"Each augment converter data should be an object array", nameof(augmentConverters));
}
if (augmentConverter.Length < 1)
{
throw new ArgumentException($"Augment converter data should include at least one item", nameof(augmentConverters));
}
object augmentConverterType = augmentConverter[0];
if (!(augmentConverterType is Type convType))
{
throw new ArgumentException($"Augment converter data should start with its type", nameof(augmentConverters));
}
if (!typeof(JsonConverter).IsAssignableFrom(convType))
{
throw new ArgumentException($"Augment converter type should inherit from JsonConverter abstract type", nameof(augmentConverters));
}
object converter = Activator.CreateInstance(convType, augmentConverter.SubArray(1, augmentConverter.Length - 1));
return (JsonConverter)converter;
}
protected abstract void WriteJsonInner(JsonWriter writer, object value, JsonSerializer serializer);
protected abstract object ReadJsonInner(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
using (AugmentedConverterScope(serializer))
{
WriteJsonInner(writer, value, serializer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
using (AugmentedConverterScope(serializer))
{
return ReadJsonInner(reader, objectType, existingValue, serializer);
}
}
private AugmentedConverterScopeMgr AugmentedConverterScope(JsonSerializer serializer)
{
// add augmented converters
for (int i = augmentConverters.Length - 1; i >= 0; i--)
{
serializer.Converters.Insert(0, augmentConverters[i]);
}
return new AugmentedConverterScopeMgr(serializer, augmentConverters.Length);
}
private class AugmentedConverterScopeMgr : IDisposable
{
private readonly JsonSerializer serializer;
private readonly int converterCount;
public AugmentedConverterScopeMgr(JsonSerializer serializer, int converterCount)
{
this.serializer = serializer;
this.converterCount = converterCount;
}
public void Dispose()
{
// remove augmented converters
for (int i = 0; i < converterCount; i++)
{
serializer.Converters.RemoveAt(0);
}
}
}
}
And then create a converter that wraps another converter's logic like so:
public class CascadeJsonConverter : CascadeJsonConverterBase
{
private readonly JsonConverter wrappedConverter;
public CascadeJsonConverter(Type wrappedConverterType, object[] wrappedConvConstructorArgs, object[] augmentConverters)
: this(CreateConverter(wrappedConverterType, wrappedConvConstructorArgs), augmentConverters.Select(FromAttributeData).ToArray())
{ }
public CascadeJsonConverter(JsonConverter wrappedConverter, JsonConverter[] augmentConverters)
: base(augmentConverters)
{
this.wrappedConverter = wrappedConverter;
}
private static JsonConverter CreateConverter(Type converterType, object[] convConstructorArgs)
{
if (!typeof(JsonConverter).IsAssignableFrom(converterType))
{
throw new ArgumentException($"Converter type should inherit from JsonConverter abstract type", nameof(converterType));
}
return (JsonConverter) Activator.CreateInstance(converterType, convConstructorArgs);
}
public override bool CanConvert(Type objectType)
{
return wrappedConverter.CanConvert(objectType);
}
protected override void WriteJsonInner(JsonWriter writer, object value, JsonSerializer serializer)
{
wrappedConverter.WriteJson(writer, value, serializer);
}
protected override object ReadJsonInner(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return wrappedConverter.ReadJson(reader, objectType, existingValue, serializer);
}
}
which can then be used to accomplish the goal in question like so
public class A {
[JsonConverter(typeof(CascadeJsonConverter), // cascading converter
typeof(OptionJsonConverter), new object[0], // converter definition for the top-level type of the property
new object[] { // collection of converter definitions to use while deserializing the contents of the property
new object[] { typeof(DateFormatConverter), "yyyy'-'MM'-'dd'T'mm':'HH':'FF.ssK" }
})]
public Option<DateTime> Time { get; set; }
}
With this, you can not only use different controllers for generic fields, but also in cases where a class needs to change the converter for some sub-property of a property's class. Neat :)
One caveat for this is that the top-level converter has to use the serializer
argument in ReadJson
and WriteJson
methods to read and write inner values instead of using JToken.Load(reader).ToObject<T>()
and JToken.FromObject(x).WriteTo(writer)
. Otherwise the inner values are read and written using unconfigured serializers.
If there is a nicer way to accomplish the same task, I'd really apprecite you sharing it!
Upvotes: 1