Search code examples
c#jsonjson.netdeserializationjson-deserialization

JSON Deserialization Handle []


I am deserializing a challenging JSON file using Newtonsoft.Json and have encountered the following property issue. This file comes from a 3rd party so I am unable to improve it so am stuck with having to deal with it as is.

It correctly represent the following property as:

{
   "propertyName":[
      {
         "value1":"value",
         "value2":"value",
         "value3":"value"
      }
   ]
}

However there is also at least one instance where this property appears as:

{
   "propertyName":[
      []
   ]
}

What I need is for any instance where the second situation above occurs it is ignored and treated as an empty array, i.e. like the following:

"propertyNames": []

For reference this is my definition of this property in its class:

[JsonProperty("propertyNames")]
public List<PropertyName> PropertyNames { get; set; }

I have handled other issues with this file using JsonConvertor and also modifications to the class definition of the JSON file.


Solution

  • Your public List<PropertyName> PropertyNames contains items that should be serialized as JSON objects, but for some reason the server is occasionally including an empty array as an item in the array. You would like to silently filter out such items.

    One way to do this would be introduce a custom JsonConverter for all List<T> types that skips array items inside a JSON array as they are being read. The following converter does the job:

    public class ArrayItemFilteringListConverter : JsonConverter
    {
        static readonly IContractResolver defaultResolver = JsonSerializer.CreateDefault().ContractResolver;
        readonly IContractResolver resolver;
    
        public ArrayItemFilteringListConverter() : this(null) { }
    
        public ArrayItemFilteringListConverter(IContractResolver resolver) => this.resolver = resolver ?? defaultResolver;
    
        public override bool CanConvert(Type objectType) => CanConvert(resolver, objectType, out _);
    
        static bool CanConvert(IContractResolver resolver, Type objectType, out Type itemType)
        {
            if (objectType.IsArray || objectType.IsPrimitive || objectType == typeof(string) || !typeof(IList).IsAssignableFrom(objectType))
            {
                itemType = null;
                return false;
            }
            itemType = objectType.GetListItemType();
            if (itemType == null)
                return false;
            if (resolver.ResolveContract(itemType) is JsonArrayContract)
                return false;
            return true;
        }
        
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (!CanConvert(serializer.ContractResolver, objectType, out var itemType))
                throw new JsonException(string.Format("Invalid collection type {0}", objectType));
            var contract = serializer.ContractResolver.ResolveContract(objectType);
    
            if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
                return null;
            else if (reader.TokenType != JsonToken.StartArray)
                throw new JsonSerializationException(string.Format("Invalid start token {0}", reader.TokenType));
            
            var list = existingValue as IList ?? (IList)contract.DefaultCreator();
            
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonToken.EndArray:
                        return list;
                    case JsonToken.StartArray:
                    case JsonToken.Comment:
                        reader.Skip();
                        break;
                    default:
                        // Here we take advantage of the fact that List<T> implements the non-generic IList interface.
                        list.Add(serializer.Deserialize(reader, itemType));
                        break;
                }
            }
            // Should not come here.
            throw new JsonSerializationException("Unclosed array at path: " + reader.Path);     
        }   
    
        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
    }
    
    public static partial class JsonExtensions
    {
        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)
        {
            while (type != null)
            {
                if (type.IsGenericType)
                {
                    var genType = type.GetGenericTypeDefinition();
                    if (genType == typeof(List<>))
                        return type.GetGenericArguments()[0];
                }
                type = type.BaseType;
            }
            return null;
        }
    }
    

    Then apply it to your data model as follows:

    public class Root    
    {
        [JsonConverter(typeof(ArrayItemFilteringListConverter))]
        public List<PropertyValue> propertyName { get; set; } 
    }
    

    Notes:

    • The converter works for any List<T> type for which the type T is not also serialized to JSON as an array. I.e. the converter cannot be applied to a List<string []> property.

    • The problem of servers that send an empty array for a default or uninitialized object seems to arise from time to time. See e.g. Deserialize JSON when a value can be an object or an empty array in which the requirement is to map an empty JSON array that should have been a JSON object to null.

    Demo fiddle here.