1
\$\begingroup\$

Here is a simplified version of my JSON.

{
    "Relations": [
        {
            "Type": "A",
            "Categories": [
                { "Name": "Identity" }
            ]
        },
        {
            "Type": "B",
            "Categories": [
                { "Name": "Identity" },
                {
                    "Name": "Contact Information",
                    "Type": "Phone"
                },
                { "Name": "Addresses" },
                { "Name": "Bank Accounts" }
            ]
        },
        {
            "Type": "C",
            "Categories": [
                { "Name": "Identity" },
                { "Name": "Contact Information" },
                { "Name": "Service Fields" }
            ]
        }
    ]
}

My code generates an IEnumerable (List, but I am open to suggestions if there is a more compatible option) that holds all distinct categories, meaning, when joining its values (using string.Join("\n", collection)) it contains the following JSON:

{ "Name": "Identity" }
{
    "Name": "Contact Information"
    "Type": "Phone"
}
{ "Name": "Addresses" }
{ "Name": "Bank Accounts" }
{ "Name": "Contact Information" }
{ "Name": "ServiceFields" }

This is the code:

List<dynamic> Method()
{
    string jString = File.ReadAllText(@"file.json");
    dynamic jObject = JsonConvert.DeserializeObject<dynamic>(jString);

    // The variable in questions
    var categories = new List<dynamic>();
    foreach (dynamic relation in jObject.Relations)
    {
        foreach (dynamic category in relation.Categories)
        {
            if (!CollectionContainsItem(categories, category))
            {
                categories.Add(category);
            }
        }
    }

    return categories;

    bool CollectionContainsItem(IEnumerable<dynamic> collection, JToken searchedItem)
    {
        foreach (var item in collection)
        {
            if (JToken.DeepEquals(item, searchedItem))
            {
                return true;
            }
        }
        return false;
    }
}

How to improve my code? It seems very cumbersome to me, especially the local method.

Note: I need a dynamic approach. I do not want to create C# classes for the JSON data.

Thanks!

\$\endgroup\$
10
  • 1
    \$\begingroup\$ Is it necessary that your result is in JSON? Couldn't you want a list of categories without json? Like : ["Identity","Contact Information",...] \$\endgroup\$
    – IEatBagels
    Commented Oct 11, 2018 at 14:12
  • \$\begingroup\$ @IEatBagels - Ignore my last comment. Yes, it should be JSON. But I will edit the question to be clearer. \$\endgroup\$ Commented Oct 11, 2018 at 14:17
  • 1
    \$\begingroup\$ Why do you create a second list (result) that ends up being a copy of the first (categories)? How is this code meant to be used (since it doesn't return anything)? And how important is input validation (e.g. do you need JToken.DeepEquals or is it safe to use (string)category.Name for comparisons)? \$\endgroup\$ Commented Oct 11, 2018 at 14:44
  • \$\begingroup\$ @PieterWitvoet - Well, I believe I had a great reason in my head for creating result, but after working for 10 straight hours, I believe I was just confused :). I will delete it from the question. I will edit the question to answer the other questions. Thanks for the notes! \$\endgroup\$ Commented Oct 11, 2018 at 14:50
  • 1
    \$\begingroup\$ @Xiaoy312: that only compares categories by name, which isn't sufficient. Replacing that GroupBy part with Distinct(JToken.EqualityComparer) should make it work as intended. \$\endgroup\$ Commented Oct 11, 2018 at 15:17

3 Answers 3

4
\$\begingroup\$

Yes, your code can indeed be simplified, as Xiaoy312 already demonstrated:

// Obtain the Relations array:
return JObject.Parse(jsonString)["Relations"]
     // Take the categories of all relations, flattening them into a single enumerable:
    .SelectMany(relation => relation["Categories"])
    // Filter out duplicate category objects (JToken.EqualityComparer uses DeepEquals internally):
    .Distinct(JToken.EqualityComparer)
    // Materialize the results into a list:
    .ToList();

This is also more efficient:

  • Using dynamic means that several things (such as method overload resolution) cannot be done at compile-time but must be done at run-time, which involves additional work.
  • Distinct uses a set internally, so it doesn't need to compare a new category against all other categories found so far - an internal hashcode lookup is often sufficient.

dynamic can be useful, but it doesn't work with extension methods (which Linq heavily depends on), so in this case it's more of a hindrance. About the only benefit here is that relation.Categories is slightly easier to write than relation["Categories"], but you do pay for that at run-time, and it handicaps your tools (no compile-time checks, no automated refactoring). Also keep in mind that dynamic is 'contagious': any expression involving something dynamic also becomes dynamic.

Another problem is that dynamic code is less self-descriptive. Is category.Name a string, or could it be a { "name": "Identity", "id": "5" } object? That sort of uncertainty makes code more difficult to maintain, at least in my experience. Static typing can be more restrictive and cumbersome, but it gives you more guarantees and tells you more about what sort of data your program expects to handle. Unless you have a good reason to stick with dynamic (in that case, throw a .Cast<dynamic>() in just before the .ToList() call) I would recommend creating a Category class.

Finally, I would make the filename a parameter and rename Method to something more descriptive such as LoadCategoriesFromJsonFile, and I would put some thoughts into error handling (what if the file doesn't exist, or if it's corrupted).

\$\endgroup\$
1
\$\begingroup\$

You can simplify CollectionContainsItem to:

  bool CollectionContainsItem(IEnumerable<dynamic> collection, JToken searchedItem)
  {
    return collection.Any(i => JToken.EqualityComparer.Equals(i, searchedItem));
  }

Another way to go is to use JSONPath like:

IEnumerable<JToken> ReadJson(string fileName)
{
  string jsonString = File.ReadAllText(fileName);
  return JObject
    .Parse(jsonString)
    .SelectTokens("$..Categories[*]")
    .Distinct(JToken.EqualityComparer);
}
\$\endgroup\$
-1
\$\begingroup\$

Maybe try a two step process:

First, convert the Json to XML. Various libraries can do the conversion. Second, use LINQ to query the XML. LINQ documentation has examples.

Expanding upon this answer. The original poster wanted less cumbersome code. Well, why reinvent the wheel? Converting the Json to XML can be done with one line of code (assuming a library function). LINQ is an query language for accessing data stored in enumerable objects like XML. All the "name" elements can be extracted in one (or more, depending on code formatting style) line of code. That is less cumbersome than the original posters code.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks! I am not at liberty of using XML. \$\endgroup\$ Commented Oct 11, 2018 at 14:19
  • 3
    \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to make this suggestion, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Commented Oct 11, 2018 at 14:34
  • \$\begingroup\$ convert the Json to XML - this is the worst possible advice. XML is evil :-] \$\endgroup\$
    – t3chb0t
    Commented Oct 11, 2018 at 17:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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