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.
4
  • I believe this is fine with json.net so I would recommend using that Commented Nov 15, 2017 at 21:53
  • 1
    Possible duplicate of JSON deserialization fails only when array is empty Commented Nov 15, 2017 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) 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? Commented Nov 15, 2017 at 22:29
  • Thanks for the feedback @dbc . Commented Nov 15, 2017 at 23:48

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.