'Deserialize json with dynamic objects that starts with pattern

I'm trying to deserialize some json that looks like this

{
   "id":"2021",
   "descriptions_bg":[
      "30231300",
      "30233160",
      "32420000",
      "30214000"
   ],
   "descriptions_cs":[
      "30231300",
      "30233160",
      "32420000",
      "30214000"
   ],
   "descriptions_da":[
      "30231300",
      "30233160",
      "32420000",
      "30214000"
   ],
   "official_title_bg":"П",
   "official_title_cs":"P",
   "official_title_da":"P",
   "auth_town_bg":"AuthTown",
   "auth_town_cs":"AuthTown",
   "auth_town_da":"AuthTown"
}

The problem here being, that there can come an infinite number of items in both descriptions_*, official_title_* and auth_town_* with different endings.

I've tried to make classes and members like public string official_title_* { get; set; }in C# to deserialize to with Newtonsoft Json, but that (of course) doesn't work.

Anyway to fix this?



Solution 1:[1]

The best option for you is to deserialize to Dictionary<string, JToken> and based on Key and Object Type you can write business logic. With the combination of Linq you can filter the Key

var json = File.ReadAllText("json1.json");

var dict = JsonConvert.DeserializeObject<Dictionary<string, JToken>>(json);

foreach(var item in dict.Where(x=>x.Key.StartsWith("descriptions_")))
{
    Console.WriteLine($"{item.Key} is type {item.Value.GetType()}");
}

foreach (var item in dict.Where(x => x.Key.StartsWith("auth_town_")))
{
    Console.WriteLine($"{item.Key} is type {item.Value.GetType()}");
}

Solution 2:[2]

An alternative method, using a class object that defines each group of properties that have a common prefix as a Dictionary<string, TValue>.

The JSON is deserialized using a custom JsonConverter that maps each group of properties in the JSON to a Property of the class, using a Dictionary<string, string>:

Dictionary<string, string> map = new Dictionary<string, string>() {
    ["Descriptions"] = "descriptions_",
    ["OfficialTitles"] = "official_title_",
    ["AuthTowns"] = "auth_town_"
};

All properties in the JSON that map to a Property in the class are added to the corresponding Dictionary and the values converted to the Type defined by the class property.

Note: you need to adapt the mapper and class Properties to the JSON. You could make the procedure more generic using some logic to determine when Property Name in the JSON belongs to the same group and generate a new Dictionary<string, TValue> to add to the class by reflection.
Or simply add more Properties to the class off-line, if you find other groups.

Call it as: (possibly giving the class a less silly name :)

var result = new SimpleSequences().Deserialize(json);

Helper class object:

private class SimpleSequences
{
    public SimpleSequences() {
        Descriptions = new Dictionary<string, long[]>();
        OfficialTitles = new Dictionary<string, string>();
        AuthTowns = new Dictionary<string, string>();
    }

    public List<SimpleSequences> Deserialize(string json)
    {
        var options = new JsonSerializerSettings() {
            Converters = new[] { new SimpleSequencesConverter() }
        };
        return JsonConvert.DeserializeObject<List<SimpleSequences>>(json, options);
    }

    public int Id { get; set; }
    public Dictionary<string, long[]> Descriptions { get; set; }
    public Dictionary<string, string> OfficialTitles { get; set; }
    public Dictionary<string, string> AuthTowns { get; set; }
}

Custom JsonConverter:

This converter handles a single object or array of objects (as shown in your question), but always return a List<SimpleSequences> (which may contain a single object). Modify as required.

public class SimpleSequencesConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) => true;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType != JsonToken.StartArray && reader.TokenType != JsonToken.StartObject) {
            throw new Exception($"Unexpected Type. Expecting Array or Object, got {reader.TokenType}");
        }
        var elementsList = new List<SimpleSequences>();

        if (reader.TokenType == JsonToken.StartObject) {
            elementsList.Add(GetPropertyValues(JObject.Load(reader)));
        }
        else {
            while (reader.Read() && reader.TokenType != JsonToken.EndArray) {
                elementsList.Add(GetPropertyValues(JObject.Load(reader)));
            }
        }
        return elementsList;
    }

    private SimpleSequences GetPropertyValues(JToken token)
    {
        var element = new SimpleSequences();
        element.Id = token["id"].ToObject<int>();
        var descriptions = token.Children().Where(t => t.Path.StartsWith(map["Descriptions"]));

        foreach (JProperty p in descriptions) {
            element.Descriptions.Add(p.Path, p.Value.ToObject<long[]>());
        }

        var titles = token.Children().Where(t => t.Path.StartsWith(map["OfficialTitles"]));
        foreach (JProperty p in titles) {
            element.OfficialTitles.Add(p.Path, p.Value.ToString());
        }

        var authTowns = token.OfType<JToken>().Where(t => t.Path.StartsWith(map["AuthTowns"]));
        foreach (JProperty p in authTowns) {
            element.AuthTowns.Add(p.Path, p.Value.ToString());
        }
        return element;
    }

    public override bool CanWrite => false;

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

    Dictionary<string, string> map = new Dictionary<string, string>() {
        ["Descriptions"] = "descriptions_",
        ["OfficialTitles"] = "official_title_",
        ["AuthTowns"] = "auth_town_"
    };
}

Solution 3:[3]

Yet another solution could be to use several LINQ2JSON queries.

For instance, if you want to get all description_xyz collections in a single list
then you can do that like this:

var semiParsedJson = JObject.Parse(json);

var descriptionProperties = semiParsedJson.Properties()
    .Where(prop => prop.Name.StartsWith("descriptions_"));

var collectionOfDescriptions =
    from desc in descriptionProperties.Values()
    select ((JArray)desc).ToObject<IEnumerable<string>>();

var flattenedDescriptions = collectionOfDescriptions.
    SelectMany(desc => desc)
    .ToList();
  • We can semi parse the json by using JObject.Parse
  • We can perform some filtering based on the properties' Name field
  • We can then get all values form each description_xyz collections as IEnumerable<string>
  • We will have at this point a collection of collections IEnumerable<IEnumerable<string>>, which we can flatten with SelectMany
  • At very end we can materialize the query with ToList

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Peter Csala