Reputation: 63
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
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