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.
CodePudding user response:
In the similar cases I prefer to create a JsonConstructor for the class
MyClass someArr=JsonConvert.DeserializeObject<MyClass>(json);
public class MyClass
{
public Dictionary<string, object> SomeArr {get; set;}
[JsonConstructor]
public MyClass(JToken SomeArr)
{
if(SomeArr.Type.ToString() != "Array")
this.SomeArr=SomeArr.ToObject<Dictionary<string,object>>();
}
}
You don't need to include all class properties in the constructor. Only the properties that need a special treatment.
CodePudding user response:
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 intermediateJArray
orJObject
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-genericIList
interface (such asHashSet<T>
), you will need to use reflection to invoke a generic method from the non-genericReadJson()
e.g. as shown in this answer to Newtonsoft Json Deserialize Dictionary as Key/Value list from DataContractJsonSerializer.
Demo fiddle here.