Jeffery Chow
Jeffery Chow

Reputation: 63

How to apply ItemConverterType when deserializing a jagged array?

I have existing attributes that properly use my 'InternedString' converter:

[JsonProperty(ItemConverterType=typeof(InternedString))]
public string[] prizeTypes;

Is there an easy way to apply it to a jagged 2d array of strings, without writing my own string[][] converter?

[JsonProperty(ItemConverterType=typeof(InternedString))]
public string[][] prizeTypesByRoomType;

(It gives me a "Can not convert Array to String" exception which is obvious, but I was sort of hoping it could apply the type recursively)

Upvotes: 1

Views: 695

Answers (1)

dbc
dbc

Reputation: 117086

Json.NET does not have functionality to apply, using attributes, a custom JsonConverter to the items of the items of a jagged array such as string [][]. ItemConverterType only applies to the items of the outermost array.

What you can do is to apply the decorator pattern to create a custom JsonConverter that wraps some inner item converter for the innermost items of jagged arrays of arbitrary depth.

First, define the following converter:

public class JaggedArrayItemConverterDecorator : JsonConverter
{
    readonly JsonConverter itemConverter;

    public JaggedArrayItemConverterDecorator(Type type) => 
        itemConverter = (JsonConverter)Activator.CreateInstance(type ?? throw new ArgumentNullException());

    public override bool CanConvert(Type objectType) => objectType.IsJaggedRankOneArray(out var itemType) && itemConverter.CanConvert(itemType);

    public override bool CanRead => itemConverter.CanRead;
    public override bool CanWrite => itemConverter.CanWrite;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        if (reader.TokenType != JsonToken.StartArray)
            throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, JsonToken.StartArray));
        var itemType = objectType.GetElementType();
        IList list = (IList)serializer.ContractResolver.ResolveContract(typeof(List<>).MakeGenericType(itemType)).DefaultCreator();
        while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
        {
            if (itemType.IsArray)
                list.Add(ReadJson(reader, itemType, null, serializer));
            else
                list.Add(itemConverter.ReadJson(reader, itemType, null, serializer));
        }
        var array = Array.CreateInstance(itemType, list.Count);
        list.CopyTo(array, 0);
        return array;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var itemType = value.GetType().GetElementType();
        writer.WriteStartArray();
        foreach (var item in (IList)value)
        {
            if (item == null)
                writer.WriteNull();
            else if (itemType.IsArray)
                WriteJson(writer, item, serializer);
            else
                itemConverter.WriteJson(writer, item, serializer);
        }
        writer.WriteEndArray();
    }
}

public static partial class JsonExtensions
{
    public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
        reader.ReadAndAssert().MoveToContentAndAssert();

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

public static class TypeExtensions
{
    public static bool IsJaggedRankOneArray(this Type type, out Type innermostItemType)
    {
        innermostItemType = null;
        var currentType = type ?? throw new ArgumentNullException(nameof(type));
        while (currentType.IsArray)
        {
            if (currentType.GetArrayRank() != 1)
                return false;
            currentType = currentType.GetElementType();
        }
        if (currentType != type)
        {
            innermostItemType = currentType;
            return true;
        }
        return false;
    }
}

Then you can apply it to jagged arrays of arbitrary depth in your model as follows:

public class Model
{
    [JsonProperty(ItemConverterType=typeof(InternedString))] // For 1d arrays you can use either ItemConverterType=typeof(InternedString) or JaggedArrayItemConverterDecorator
    public string[] prizeTypes { get; set; }

    [JsonConverter(typeof(JaggedArrayItemConverterDecorator), typeof(InternedString))]
    public string[] prizeTypesByRoomType { get; set; }

    [JsonConverter(typeof(JaggedArrayItemConverterDecorator), typeof(InternedString))]
    public string[][] prizeTypesByRoomType2d { get; set; }

    [JsonConverter(typeof(JaggedArrayItemConverterDecorator), typeof(InternedString))]
    public string[][][] prizeTypesByRoomType3d { get; set; }

    [JsonConverter(typeof(JaggedArrayItemConverterDecorator), typeof(InternedString))]
    public string[][][][] prizeTypesByRoomType4d { get; set; }
    
    [JsonProperty(ItemConverterType=typeof(InternedString))] // ItemConverterType works for multidimensional arrays
    public string[,] prizeTypesMultidimensional { get; set; }
}

Notes:

  • The converter is implemented for arrays, not lists. If you need to support jagged lists, see How to apply a custom JsonConverter to the values inside a list inside a dictionary?.

  • The converter is not implemented for multidimensional arrays such as string [,], or for jagged arrays containing multidimensional arrays. However, ItemConverterType does in fact support multidimensional arrays, so unless you have jagged arrays of multidimensional arrays or vice versa you should be all set.

Demo fiddle here.

Upvotes: 2

Related Questions