5

I've a valid JSON object that has a JSON array in it. The JSON array doesn't have curly braces and contains a comma separated list of mixed type in it. It looks like this:

{
    "ID": 17,
    "Days": 979,
    "Start_Date": "10/13/2012",
    "End_Date": "11/12/2012",
    "State": "",
    "Page": 1,
    "Test": "Valid",
    "ROWS": [
        [441210, "A", "12/31/2009", "OK", "Done", "KELLEY and Co'", "12/31/2009", "06/29/2010", "TEXAS", "Lawyers", 6, "", "<img src=\"/includes/images/Icon_done.gif\" border=\"0\" alt=\"Done\" title=\"Done\" />"],
        [441151, "B", "12/31/2009", "No", "In-process", "Sage & Co", "12/31/2009", "06/29/2010", "CALIFORNIA", "Realtor", 6, "", "<img src=\"/includes/images/Icon_InProcess.gif\" border=\"0\" alt=\"In Process\" title=\"In Process\" />"]
    ]
}

I've created a class to reflect the JSON structure, having a List for the complex array:

class myModel
{
    public int ID { get; set; }
    public int Days { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public string State { get; set; }
    public string Page { get; set; }
    public string Test { get; set; }
    List<ChildModel> Rows { get; set; }
}

I've tested it with a List of a List too:

List<List<ChildModel>> Rows { get; set; }

And the child model like this:

class ChildModel
{
    public int ID { get; set; }
    public string StatusId { get; set; }
    public DateTime ContactDate { get; set; }
    public string State { get; set; }
    public string Status { get; set; }
    public string CustomerName { get; set; }
    public DateTime WorkStartDate { get; set; }
    public DateTime WorkEndDate { get; set; }
    public string Territory { get; set; }
    public string CustType { get; set; }
    public int JobOrder { get; set; }
    public string Filler { get; set; }
    public string Link { get; set; }
}

In my program.cs file, I'm deserializing it like this:

using (StreamReader r = new StreamReader(@"D:\01.json"))
{
    myModel items = JsonConvert.DeserializeObject<myModel>(r.ReadToEnd());
}

When I run this program, the child object (Rows) is always null. What am I doing wrong?

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
SJaka
  • 712
  • 13
  • 40
  • So you're saying that `ChildModel` is serialized as an array rather than an object? Is the serialization out of your control? – 15ee8f99-57ff-4f92-890c-b56153 Nov 29 '17 at 15:27
  • 2
    I guess it's because there's no mapping between the stuff in your lists and your ChildModel class. How would the deserializer know what value to put in what property of your model? If possible, fix the serialization, otherwise build a custom parser and pray to god they don't change the order of the data in the lists. – Erik Kinding Nov 29 '17 at 15:41
  • 1
    To deserialize that array to a `ChildModel` you could use `ObjectToArrayConverter` from [C# JSON.NET - Deserialize response that uses an unusual data structure](https://stackoverflow.com/q/39461518/3744182). – dbc Nov 29 '17 at 18:13
  • @EdPlunkett, yes, I can't control that serialization. I'm only consuming a service provided by a vendor. – SJaka Nov 29 '17 at 18:33
  • Try this solution https://stackoverflow.com/questions/63982417/deserialize-json-array-of-array-to-list-of-string-in-c-sharp. – Ali Potasyum Jun 01 '22 at 12:08

2 Answers2

9

Json.Net does not have a facility to automatically map an array into a class. To do so you need a custom JsonConverter. Here is a generic converter that should work for you. It uses a custom [JsonArrayIndex] attribute to identify which properties in the class correspond to which indexes in the array. This will allow you to easily update your model if the JSON changes. Also, you can safely omit properties from your class that you don't need, such as Filler.

Here is the code:

public class JsonArrayIndexAttribute : Attribute
{
    public int Index { get; private set; }
    public JsonArrayIndexAttribute(int index)
    {
        Index = index;
    }
}

public class ArrayToObjectConverter<T> : JsonConverter where T : class, new()
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JArray array = JArray.Load(reader);

        var propsByIndex = typeof(T).GetProperties()
            .Where(p => p.CanRead && p.CanWrite && p.GetCustomAttribute<JsonArrayIndexAttribute>() != null)
            .ToDictionary(p => p.GetCustomAttribute<JsonArrayIndexAttribute>().Index);

        JObject obj = new JObject(array
            .Select((jt, i) =>
            {
                PropertyInfo prop;
                return propsByIndex.TryGetValue(i, out prop) ? new JProperty(prop.Name, jt) : null;
            })
            .Where(jp => jp != null)
        );

        T target = new T();
        serializer.Populate(obj.CreateReader(), target);

        return target;
    }

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

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

To use the converter, you need to mark up your ChildModel class as shown below:

[JsonConverter(typeof(ArrayToObjectConverter<ChildModel>))]
class ChildModel
{
    [JsonArrayIndex(0)]
    public int ID { get; set; }
    [JsonArrayIndex(1)]
    public string StatusId { get; set; }
    [JsonArrayIndex(2)]
    public DateTime ContactDate { get; set; }
    [JsonArrayIndex(3)]
    public string State { get; set; }
    [JsonArrayIndex(4)]
    public string Status { get; set; }
    [JsonArrayIndex(5)]
    public string CustomerName { get; set; }
    [JsonArrayIndex(6)]
    public DateTime WorkStartDate { get; set; }
    [JsonArrayIndex(7)]
    public DateTime WorkEndDate { get; set; }
    [JsonArrayIndex(8)]
    public string Territory { get; set; }
    [JsonArrayIndex(9)]
    public string CustType { get; set; }
    [JsonArrayIndex(10)]
    public int JobOrder { get; set; }
    [JsonArrayIndex(12)]
    public string Link { get; set; }
}

Then just deserialize as usual and it should work as you wanted. Here is a demo: https://dotnetfiddle.net/n3oE3L

Note: I did not implement WriteJson, so if you serialize your model back to JSON, it will not serialize back to the array format; instead it will use the default object serialization.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • This answer works great. However, be aware it's slow, probably due to the reflection code. In my parsing of 253118837 records this code took 80 minutes. If performance is a concern consider giving up strong typing and use dynamic and indexing e.g. `dynamic items = JsonConvert.DeserializeObject(r.ReadToEnd()); var id = items.ROWS[0]; ...`. That code decreased my 80 minute query to 11 minutes. – Lee Richardson Dec 03 '20 at 21:00
  • It can be improved by caching `propsByIndex` per type – dizarter Apr 26 '22 at 00:28
2

Use this to deserialize your data into a json object...

public class Rootobject
{
    public int ID { get; set; }
    public int Days { get; set; }
    public string Start_Date { get; set; }
    public string End_Date { get; set; }
    public string State { get; set; }
    public int Page { get; set; }
    public string Test { get; set; }
    public object[][] ROWS { get; set; }
}

...and then create a utility function that converts the object[][] to your desired target object...

(created with visual studio: Edit -> Paste Special -> JSON classes)

Michael
  • 1,931
  • 2
  • 8
  • 22