0

I am consuming some JSON in a C# console application and for some of the data, there is an array of options. Example JSON:

{
    "FIELD_NAME": "Survey",
    "VALUE": "",
    "FIELD_ID": 1234,
    "OPTIONS":[
        { "VALUE": "GENERAL", "DISPLAY_ORDER": 1, "DISPLAY": "GENERAL" },
        { "VALUE": "HPEFS",   "DISPLAY_ORDER": 3, "DISPLAY": "HPEFS" },
        { "VALUE": "NONE",    "DISPLAY_ORDER": 3, "DISPLAY": "NONE" }]
}

But sometimes for records in the JSON the OPTIONS is empty:

{"FIELD_NAME":"Product_Node3","VALUE":"","FIELD_ID":1740,"OPTIONS":{}}

As you can see the options is set to {} but it is my understanding that {} is an empty object, not an empty array.

When I try deserialize to a POCO I get an exception complaining that it requires a JSON array in the OPTIONS property.

My field class:

public class Field
{
    public string FIELD_NAME { get; set; }
    public string VALUE { get; set; }
    public int FIELD_ID { get; set; }
    public List<Option> OPTIONS { get; set;
    }
}

And options class:

public class Option
{
    public string VALUE { get; set; }
    public int DISPLAY_ORDER { get; set; }
    public string DISPLAY { get; set; }
}

The code which causes this exception is:

            var stringTest = File.ReadAllText("json.txt");
            var data = JsonConvert.DeserializeObject<List<Field>>(stringTest);

Exception is:

Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[testproj.Option]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
andrewb
  • 2,995
  • 7
  • 54
  • 95
  • I believe this is fine with json.net so I would recommend using that – ghostbust555 Nov 15 '17 at 21:53
  • 1
    Possible duplicate of [JSON deserialization fails only when array is empty](https://stackoverflow.com/questions/22027041/json-deserialization-fails-only-when-array-is-empty) – mjwills Nov 15 '17 at 22:01
  • @mjwills - not quite a duplicate. In that question (as well as [*How to handle both a single item and an array for the same property using JSON.net*](https://stackoverflow.com/q/18994685/3744182)) the OP wants to deserialize the object as a one-item collection containing that object. Here I believe OP wants to **entirely skip** the object. I could be wrong though, so could OP please confirm? – dbc Nov 15 '17 at 22:29
  • Thanks for the feedback @dbc . – mjwills Nov 15 '17 at 23:48

1 Answers1

1

Json.NET will throw an exception when the expected JSON value type (array, collection or primitive) does not match the observed value type. Since, in the case of your List<Option> OPTIONS, you want unexpected value types to be skipped, you will need to create a custom JsonConverter such as the following:

public class TolerantCollectionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsPrimitive || objectType == typeof(string) || objectType.IsArray)
            return false;
        return objectType.GetCollectionItemTypes().Count() == 1;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
        else if (reader.TokenType == JsonToken.StartArray)
        {
            existingValue = existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
            serializer.Populate(reader, existingValue);
            return existingValue;
        }
        else
        {
            reader.Skip();
            return existingValue;
        }
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public static class TypeExtensions
{
    /// <summary>
    /// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.IsInterface)
            return new[] { type }.Concat(type.GetInterfaces());
        else
            return type.GetInterfaces();
    }

    public static IEnumerable<Type> GetCollectionItemTypes(this Type type)
    {
        foreach (Type intType in type.GetInterfacesAndSelf())
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                yield return intType.GetGenericArguments()[0];
            }
        }
    }
}

Then apply it to Field as follows:

public class Field
{
    public string FIELD_NAME { get; set; }
    public string VALUE { get; set; }
    public int FIELD_ID { get; set; }

    [JsonConverter(typeof(TolerantCollectionConverter))]
    public List<Option> OPTIONS { get; set; }
}

Or use it for all collections via JsonSerializerSettings:

var settings = new JsonSerializerSettings
{
    Converters = { new TolerantCollectionConverter() },
};
var obj = JsonConvert.DeserializeObject<Field>(stringTest, settings);

Notes:

  • The converter only works for collections that are writable, since it allocates the collection first and then populates it. For read-only collections or arrays you need to populate a List<T> first then allocate the read-only collection or array from it.

  • My assumption here is that you want to ignore an empty object when an array value is expected. If instead you want to deserialize the object into a collection item then add that to the returned collection you could use SingleOrArrayConverter<T> from How to handle both a single item and an array for the same property using JSON.net.

  • The root JSON container shown in your question is an object -- an unordered set of name/value pairs that begins with { and ends with } -- rather than an array. Thus you need to deserialize it as a Field not a List<Field>.

Sample fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340