Home > OS >  Can I set my own default List<T> converter in JSON.net (without attributes)
Can I set my own default List<T> converter in JSON.net (without attributes)

Time:08-06

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 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.

  • Related