1

I have the following JSON file, which I am having problems parsing.

{
  "version": 2,
  "versioned_files": [
    {
      "DB": [
        "Table0",
        [
          {
            "version": 0,
            "fields": [
              {
                "name": "key",
                "type": "StringU8"
              },
              {
                "name": "value",
                "type": "Float"
              }
            ],
            "localised": []
          }
        ]
      ]
    },
    {
      "NoDbObject": [
        {
          "version": 1,
          "fields": [
            {
              "name": "objectProp",
              "type": "StringU8"
            }
          ],
          "localised": []
        }
      ]
    }
  ]
}

https://json2csharp.com/ generates the following code for me, which is not very helpful:

// Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); 
public class Root
{
    public int version { get; set; } 
    public List<List<object>> files { get; set; } 
}

Most of what I have tried has given me a null Files list or the following error:

Newtonsoft.Json.JsonSerializationException:
Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'RonParser.VersionedFile' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly. To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array. Path 'Files[0]', line 4, position 5.

How do I construct the data objects here?

user1038502
  • 767
  • 1
  • 9
  • 21
  • 4
    The first thing all `field_type` are wrong, should be `"field_type": "uint8"`, `"string"` ect. – Mohammed Sajid Jul 02 '20 at 10:04
  • 1
    **always** validate your JSON-input as **first** step in debugging. yours isn't valid. – Franz Gleichmann Jul 02 '20 at 10:07
  • 2
    Don't rely on a code generator. While such tools can come in handy, you should most definitely not use one unless you understand how to write its output by hand. – Aluan Haddad Jul 02 '20 at 10:08
  • @FranzGleichmann I have validated the file, but I did some manual editing so I didnt have to post the 100k line file, made a small error there. Update the question and thanks for pointing out. – user1038502 Jul 02 '20 at 10:12
  • with updated `json` try to cast, like `Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse);` i think, this `Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse);` will gives you build error. or change`Root` to dyamic or object. – Mohammed Sajid Jul 02 '20 at 10:13
  • I could not get that to work. I can load the whole thing if I use dynamic objects, but that makes it hard to use. Its mainly the dictonary part related to "table0" which has me confused. I would have thought I could have done Dictionary> but that just throws an exception – user1038502 Jul 02 '20 at 10:25
  • You need to construct class to consume your json. refer - https://stackoverflow.com/questions/4611031/convert-json-string-to-c-sharp-object – SSD Jul 02 '20 at 10:42
  • That I understand. The problem is how to construct the data class. I am unsure how the content in versioned_files should be modeled – user1038502 Jul 02 '20 at 10:51
  • are you sure you don't mean it to be ? "DB": [ {"Table0": [{ ? – InUser Jul 02 '20 at 11:04
  • Its not my format, so Its not up to me to decide. According to https://jsonlint.com/ its a valid way of defining a json file. – user1038502 Jul 02 '20 at 11:08
  • Is `versioned_files` structure the same all the time? – Pavel Anikhouski Jul 02 '20 at 11:19
  • so you can use public class VersionedFile { public List DB { get; set; } } public class Root { public int version { get; set; } public List versioned_files { get; set; } } – InUser Jul 02 '20 at 11:19
  • in this case you will receive table0 as first member and all the data inside as the second member of the DB array.. – InUser Jul 02 '20 at 11:20
  • @pavel yes, its the same all the time, at least as far as i know – user1038502 Jul 02 '20 at 11:21

1 Answers1

1

I have to say, this is one of the more unfriendly JSON formats I've seen in the wild. The main problem is that some of the arrays contain mixed types, which makes it difficult to declare classes to represent them. JSON class generators typically don't know what to do in this situation, so they just default to using List<object>, as you have seen. Many times you will need to come up with a sensible class model yourself and then use a custom JsonConverter to populate it.

Here is the model I came up with for your JSON:

class RootObject
{
    public int Version { get; set; }

    [JsonProperty("versioned_files")]
    public List<VersionedFile> VersionedFiles { get; set; }
}

[JsonConverter(typeof(VersionedFileConverter))]
class VersionedFile
{
    public string Key { get; set; }
    public string Label { get; set; }
    public List<Item> Items { get; set; }
}

class Item
{
    public int Version { get; set; }
    public List<Field> Fields { get; set; }
    public List<object> Localised { get; set; }
}

class Field
{
    public string Name { get; set; }
    public string Type { get; set; }
}

Some notes about the model:

  • In the JSON, versioned_files array elements are objects which each contain exactly one key, but the key is different in each. I did not know whether this key represents a name or a type or what, so I just called it Key and rolled it into the VersionedFile class.
  • There are two different formats for the versioned files, which I'll refer to as "DB" and "Non-DB" format. The "Non-DB" format is an array of objects. The "DB" format is an array containing two elements: a string followed by an array of objects with the same shape as the "Non-DB" format. I made the assumption that the "DB" array will never contain any additional elements, and that the string is merely an additional label or title related to the list of Items. Rather than create separate classes for these two formats, I just decided to reuse the same VersionedFile class and add an extra Label property to capture the string. Label will always be null for the "Non-DB" format.

Below is the code for the VersionedFileConverter. The converter uses the LINQ-to-JSON API to determine which format is being used for each VersionedFile object and then populate it accordingly. It also handles the dynamic keys.

public class VersionedFileConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(VersionedFile);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = JObject.Load(reader);
        JProperty prop = obj.Properties().First();

        VersionedFile file = new VersionedFile 
        {
            Key = prop.Name,
            Items = new List<Item>() 
        };

        JArray array = (JArray)prop.Value;
        if (array.Count > 0)
        {
            if (array[0].Type == JTokenType.String)
            {
                file.Label = (string)array[0];
                file.Items = array[1].ToObject<List<Item>>(serializer);
            }
            else
            {
                file.Items = array.ToObject<List<Item>>(serializer);
            }
        }

        return file;
    }

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

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

With all this in place, we can deserialize and dump out the data like this:

var root = JsonConvert.DeserializeObject<RootObject>(json);

Console.WriteLine("Root version: " + root.Version);
Console.WriteLine("Versioned files:");
foreach (var vf in root.VersionedFiles)
{
    Console.WriteLine("  Key: " + vf.Key);
    Console.WriteLine("  Label: " + (vf.Label ?? "(none)"));
    Console.WriteLine("  Items:");
    foreach (var item in vf.Items)
    {
        Console.WriteLine("    Version: " + item.Version);
        Console.WriteLine("    Fields:");
        foreach (var field in item.Fields)
        {
            Console.WriteLine("      Field name: " + field.Name);
            Console.WriteLine("      Field type: " + field.Type);
            Console.WriteLine();
        }
    }
}

Which gives the following ouptut:

Root version: 2
Versioned files:
  Key: DB
  Label: Table0
  Items:
    Version: 0
    Fields:
      Field name: key
      Field type: StringU8

      Field name: value
      Field type: Float

  Key: NoDbObject
  Label: (none)
  Items:
    Version: 1
    Fields:
      Field name: objectProp
      Field type: StringU8

Working demo here: https://dotnetfiddle.net/Slkufm

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • If the array is heterogenous, you might consider declaring it as `dynamic[]`. You _could_ use also use `Newtonsoft.Json.Linq.Object[]` – Aluan Haddad Jul 02 '20 at 20:22
  • That worked close to perfect, but while running it I noticed that I have two other objects in my array as well. They dont contain the "table name" attribute, but apart from that they are the same. Is there a way to handle that? I have updated the original question to reflect the new object – user1038502 Jul 02 '20 at 21:08
  • 1
    Yes, if we ditch the `ArrayToObjectConverter` and use a custom JsonConverter instead, we can handle both formats. This will also allow us to simplify the model and get rid of the `Dictionary`. I've updated my answer. – Brian Rogers Jul 03 '20 at 19:43
  • Well done and very helpful! Learned a lot about custom json parsing! Thanks – user1038502 Jul 03 '20 at 22:05
  • No problem; glad I could help! – Brian Rogers Jul 03 '20 at 22:54