Service which I work with uses strange serialization. When array is empty it looks like this:
"SomeArr":[]
But when 'SomeArr' has items it looks like this:
"SomeArr":
{
"item1": { "prop1":"value1" },
"item2": { "prop1":"value1" }
...
}
So it's not even array now but JObject with properties instead of array enumerators
I have this converter that must be applied to all properties with List
type
public class ArrayObjectConverter<T> : JsonConverter<List<T>>
{
public override void WriteJson(JsonWriter writer, List<T>? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override List<T>? ReadJson(JsonReader reader, Type objectType, List<T>? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
List<T> result = new();
if (reader.TokenType == JsonToken.StartArray)
{
var jArray = JArray.Load(reader);
//'LINQ Select' because sometimes arrays are normal
//So if I set this converter as default we select objects from this array
return jArray.Select(jt => jt.ToObject<T>()!).ToList();
}
else
{
var jObject = JObject.Load(reader);
foreach (var kvp in jObject)
{
var obj = kvp.Value!.ToObject<T>()!;
result.Add(obj);
}
return result;
}
}
}
So how I can set this converter as default (e.g. in serializer.settings). The problem is this converter is generic type and I can't set in settings without generic argument.
Of course I can put [JsonConverter(typeof(ArrayObjectConverter<T>))]
attribute for every collection. But my json classes already have a lot of boilerplate. Any suggestions?
P.S. The solution should be as optimized as possible because the speed of deserialization is very important.
You can take advantage of the fact that List<T>
implements the non-generic interface IList
to create a non-generic JsonConverter
for all List<T>
types:
public class ArrayObjectConverter : JsonConverter
{
public override bool CanConvert(Type t) => t.GetListItemType() != null;
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
System.Diagnostics.Debug.Assert(objectType.GetListItemType() != null);
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
IList value = existingValue as IList ?? (IList)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator!();
if (reader.TokenType == JsonToken.StartArray)
{
serializer.Populate(reader, value);
}
else if (reader.TokenType == JsonToken.StartObject)
{
var itemType = objectType.GetListItemType().ThrowOnNull();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
// Eat the property name
reader.AssertTokenType(JsonToken.PropertyName).ReadToContentAndAssert();
// Deserialize the property value and add it to the list.
value.Add(serializer.Deserialize(reader, itemType));
}
}
else
{
throw new JsonSerializationException(string.Format("Unknown token type {0}", reader.TokenType));
}
return value;
}
}
public static partial class JsonExtensions
{
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
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 Type? GetListItemType(this Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType!;
}
return null;
}
}
public static partial class ObjectExtensions
{
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException(nameof(value));
}
Notes:
Your question mentions The solution should be as optimized as possible so the converter deserializes directly from the JsonReader
without needing to pre-load anything into intermediate JArray
or JObject
instances.
The converter should work for subclasses of List<T>
as well.
If you need to support types that implement ICollection<T>
types but do not also implement the non-generic IList
interface (such as HashSet<T>
), you will need to use reflection to invoke a generic method from the non-generic ReadJson()
e.g. as shown in this answer to Newtonsoft Json Deserialize Dictionary as Key/Value list from DataContractJsonSerializer.
Demo fiddle here.