4
\$\begingroup\$

I have below json which I need to deserialize in C# -

{
   "clientSettings":[
      {
         "clientId":12345,
         "entries":[
            {
               "key":"abc",
               "value":false
            },
            {
               "key":"def",
               "value":false
            },
            {
               "key":"ghi",
               "value":false
            }
         ]
      },
      {
         "clientId":9876,
         "entries":[
            {
               "key":"lkmn",
               "value":false
            }
         ]
      }
   ],
   "productSettings":[
      {
         "productId":11,
         "entries":[
            {
               "key":"jkl",
               "value":true
            },
            {
               "key":"mno",
               "value":true
            }
         ]
      },
      {
         "productId":12,
         "entries":[
            {
               "key":"jkl",
               "value":true
            },
            {
               "key":"mno",
               "value":true
            }
         ]
      }
   ],
   "customerSettings":[
      {
         "key":"enableData",
         "value":false
      },
      {
         "key":"minPriceValue",
         "value":"10.28"
      },
      {
         "key":"presentData",
         "value":"AEGIS"
      }
   ],
   "thothTest":{
      "9876":[
         "K"
      ],
      "5431":[
         "A",
         "L"
      ],
      "5123":[
         "L"
      ]
   },
   "osirisTest":{
      "7678":[
         "K"
      ]
   }
}

Below is the classes I created to deserialzie json into -

public class ProcessHolder : Holder
{
    public IDictionary<int, ISet<string>> OsirisTest { get; set; }
    public IDictionary<int, ISet<string>> ThothTest { get; set; }
}

public class Holder
{
    public IList<Mapping> CustomerSettings { get; set; }
    public IList<ClientSettingsMapping> ClientSettings { get; set; }
    public IList<ProductSettingsMapping> ProductSettings { get; set; }
}

public class Mapping
{
    public string Key { get; set; }
    public object Value { get; set; }
}

public class ProductSettingsMapping : Mapping
{
    public int ProductId { get; set; }
}

public class ClientSettingsMapping : Mapping
{
    public int ClientId { get; set; }
}
  • I want to load all customerSettings values into CustomerSettings object of Holder class.
  • Similarly I want to load all clientSettings values into ClientSettings object of Holder class.
  • Similarly all productSettings values into ProductSettings object of Holder class.
  • Similarly thothTest values into ThothTest and osirisTest values into OsirisTest object.

Note: Above POCO classes used to work with old json format and we were deserializing it directly using Newton JSON library but for some internal reason we are working on redesigning the json into new json which is what I shared above in my question. And now with this new json design we cannot directly deserialize it to my above POCO classes so that's why I came up with below code to do it manually. Since above POCO classes are used at so many places so thats why I don't want to touch that interface.

Below is my working code which manually parses that json and populate above classes. I wanted to see if there is any way we can improve below code base as it looks like I am repeating lot of things. I think it can be organized by having some helper methods which can do the job for me but not able to figure it out on how to do this?

public static void Main(string[] args)
{
    var jsonContent = File.ReadAllText("/beta/Downloads/test.json");
    var parsed = JObject.Parse(jsonContent);

    var parsedClientSettings = parsed["clientSettings"];
    List<ClientSettingsMapping> clientSettings = new List<ClientSettingsMapping>();
    foreach (var parsedClientSetting in parsedClientSettings)
    {
        var clientId = parsedClientSetting.Value<int>("clientId");
        foreach (var entry in parsedClientSetting["entries"])
        {
            clientSettings.Add(new ClientSettingsMapping { ClientId = clientId, Key = entry["key"].ToString(), Value = entry["value"].ToString() });
        }
    }
    var parsedproductSettings = parsed["productSettings"];
    List<ProductSettingsMapping> productSettings = new List<ProductSettingsMapping>();
    foreach (var parsedproductSetting in parsedproductSettings)
    {
        var productId = parsedproductSetting.Value<int>("productId");
        foreach (var entry in parsedproductSetting["entries"])
        {
            productSettings.Add(new ProductSettingsMapping { ProductId = productId, Key = entry["key"].ToString(), Value = entry["value"].ToString() });
        }
    }
    var parsedCustomerSettings = parsed["customerSettings"];
    List<Mapping> customerSettings = new List<Mapping>();
    foreach (var entry in parsedCustomerSettings)
    {
        customerSettings.Add(new Mapping { Key = entry["key"].ToString(), Value = entry["value"].ToString() });
    }
    var parsedthothTests = parsed["thothTest"];
    IDictionary<int, ISet<string>> thothTest = new Dictionary<int, ISet<string>>();
    foreach (JProperty x in (JToken)parsedthothTests)
    {
        int name = int.Parse(x.Name);
        JToken value = x.Value;
        ISet<string> values = new HashSet<string>();
        foreach (var val in value)
        {
            values.Add((string)val);
        }
        thothTest.Add(name, values);
    }
    var parsedOsirisTests = parsed["osirisTest"];
    IDictionary<int, ISet<string>> osirisTest = new Dictionary<int, ISet<string>>();
    foreach (JProperty x in (JToken)parsedOsirisTests)
    {
        int name = int.Parse(x.Name);
        JToken value = x.Value;
        ISet<string> values = new HashSet<string>();
        foreach (var val in value)
        {
            values.Add((string)val);
        }
        osirisTest.Add(name, values);
    }
    ProcessHolder processHolder = new ProcessHolder() { ClientSettings = clientSettings, ProductSettings = productSettings, CustomerSettings = customerSettings, ThothTest = thothTest, OsirisTest = osirisTest };
    
    // now use "processHolder" object here
}
\$\endgroup\$
6
\$\begingroup\$

You can take advantage of the ToObject() method of the JToken.

With that your parsing logic could be simplified to this:

var jsonContent = File.ReadAllText("/beta/Downloads/test.json");
var parsed = JObject.Parse(jsonContent);

//ClientSettings needs special handling
var parsedClientSettings = parsed["clientSettings"];
var clientSettings = new List<ClientSettingsMapping>();
foreach (var parsedClientSetting in parsedClientSettings)
{
    var clientId = parsedClientSetting.Value<int>("clientId");
    var entries = parsedClientSetting["entries"].ToObject<List<Mapping>>();
    clientSettings.AddRange(entries
        .Select(entry => new ClientSettingsMapping { ClientId = clientId, Key = entry.Key, Value = entry.Value }));
}

//ProductSettings needs special handling
var parsedproductSettings = parsed["productSettings"];
var productSettings = new List<ProductSettingsMapping>();
foreach (var parsedproductSetting in parsedproductSettings)
{
    var productId = parsedproductSetting.Value<int>("productId");
    var entries = parsedproductSetting["entries"].ToObject<List<Mapping>>();
    productSettings.AddRange(entries
        .Select(entry => new ProductSettingsMapping {ProductId = productId, Key = entry.Key, Value = entry.Value}));
}

var processHolder = new ProcessHolder
{
    ClientSettings = clientSettings,
    ProductSettings = productSettings,
    CustomerSettings = parsed["customerSettings"].ToObject<List<Mapping>>(),
    ThothTest = parsed["thothTest"].ToObject<Dictionary<int, ISet<string>>>(),
    OsirisTest = parsed["osirisTest"].ToObject<Dictionary<int, ISet<string>>>()
};

clientSettings/entries

  • Here you can use ToObject<List<Mapping>
    • then you can use LINQ to construct a list of ClientSettingsMapping

productSettings/entries

  • Here you can use ToObject<List<Mapping>
    • then you can use LINQ to construct a list of ProductSettingsMapping

customerSettings

  • Here you can use ToObject<List<Mapping>

thothTest and osirisTest

  • Here you can use ToObject<Dictionary<int, ISet<string>>>

UPDATE #1: Adding helper method to reduce code duplication

In order to reduce code duplication (the Client and Product settings are handled almost in the same way) we can introduce the following helper method:

private List<T> ParseSettings<T>(JToken parsed, string settingsNodeName, string idNodeName, Func<(int id, string key, object value), T> mapper)
{
    var rawSettings = parsed[settingsNodeName];
    var settings = new List<T>();
    foreach (var setting in rawSettings)
    {
        var id = setting.Value<int>(idNodeName);
        var entries = setting["entries"].ToObject<List<Mapping>>();
        settings.AddRange(entries
            .Select(entry => mapper((id, entry.Key, entry.Value))));
    }

    return settings;
}

Its usage would look like this:

var productSettings = ParseSettings(parsed, "productSettings", "productId",
        mapping=> new ProductSettingsMapping {ProductId = mapping.id, Key = mapping.key, Value = mapping.value});
var clientSettings = ParseSettings(parsed, "clientSettings", "clientId",
        mapping=> new ClientSettingsMapping {ClientId = mapping.id, Key = mapping.key, Value = mapping.value});

UPDATE #2: Adding an alternative implementation (uses linq query expression syntax) for ParseSettings:

private static List<T> ParseSettings<T>(JToken parsed, string settingsNodeName, string idNodeName, 
    Func<(int id, string key, object value), T> mapper)
    =>
        (from setting in parsed[settingsNodeName]
         let id = setting.Value<int>(idNodeName)
         let entries = setting["entries"].ToObject<List<Mapping>>()
            from entry in entries
            select mapper((id, entry.Key, entry.Value)))
        .ToList();
\$\endgroup\$
5
  • 1
    \$\begingroup\$ I wasn't aware of ToObject() method of the JToken. This is nice. Thanks for showing me this. Also do you think we can simplify clientSettings and productSettings into some helper methods so that we don't have to duplicate same code which is being used in them? \$\endgroup\$ – David Todd Oct 6 '20 at 14:03
  • \$\begingroup\$ @DavidTodd I've updated my answer to include that helper method. \$\endgroup\$ – Peter Csala Oct 6 '20 at 14:23
  • 1
    \$\begingroup\$ Looks like there is a bug in ParseSettings method where it only keeps one object in it but I have multiple json objects for productSettings and clientSettings. It seems to overwriting previous entries. \$\endgroup\$ – David Todd Oct 19 '20 at 16:52
  • \$\begingroup\$ For example: productSettings object only has 12 as productId and its entries. 11 as the productId and its entries is missing. \$\endgroup\$ – David Todd Oct 19 '20 at 17:03
  • \$\begingroup\$ @DavidTodd Yepp, you are right, sorry for the inconvenience. We need to use AddRange. I've updated my answer, please check it again. \$\endgroup\$ – Peter Csala Oct 20 '20 at 9:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.