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