Gman
Gman

Reputation: 1876

How to specify custom json converter for the generic argument of a property type

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 Options in an effort to avoid null comparisons, and this issue may arise for other types in the future anyway.

Upvotes: 1

Views: 1209

Answers (1)

Gman
Gman

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

Related Questions