Search code examples
c#jsonloopsdictionaryjavascriptserializer

Issues Iterating through a dynamic object generated from a JSON file


I have a json file that (for the sake of this question) I simplified down:

{
  "servername": {
    "goodvolumes": [
      {
        "Name": "vol1",
        "State": "online",
        "Size": "12.0 TB"
      },
      {
        "Name": "vol2",
        "State": "online",
        "Size": "10.0 TB"
      }
    ],
    "BadVolumes": {
      "Name": "badVol",
      "State": "offline",
      "TotalSize": "120GB"
    }
  }
}

When this is being read into my C# I have an data object of type System.Collections.Generic.Dictionary<string,object>.

I am then iterating through the object and creating a model object that I am going to be passing to my view.

This is posing a difficulty for me. Here is an example of how I am iterating through the json.

//top level of my JSON - the serverName
foreach(serverName in jsonData) 
{
    //record the serverName

    //second level of my JSON - the notification name
    foreach(notification in serverName.Value)
    {
        //record the notification name

        //3rd Level of my JSON - iterating the entries
        foreach(entry in notification.Value)
        {
            //Iterating all the values in an entry
            foreach(entryValue in entry)
            {
               //record values in each entry
            }

        }
    }
}

The issue that I am having is when iterating through the third level if there is only 1 entry.

By the nature of JSON, if a notification type has multiple entries, then inside of my notifications.Value there will be another list of collections. In this case, my code works like a charm.

However, if there is only 1 entry for a notification,notification.value actually contains the list of KeyValuePair for all of the values inside the single entry. So the 3rd level of iteration isn't working. It is actually trying to iterate through the values at that point.

Unfortunately the json I am working with cannot be modified, this is the format that I am going to be receiving it in.

I hope that this accurately explains the issue that I am having. I know where the issue occurs, I am just not sure at all how to get around it.


Solution

  • Firstly, you might consider switching to . If you do, you could deserialize directly to a Dictionary<string, Dictionary<string, List<Dictionary<string, string>>>> using SingleOrArrayConverter<Dictionary<string, string>> from How to handle both a single item and an array for the same property using JSON.net. This also avoids the proprietary date format used by JavaScriptSerializer.

    Using JavaScriptSerializer, you will need to know some details of how it deserializes arbitrary JSON data. Specifically, it deserializes JSON objects as IDictionary<string, object> and JSON arrays as some sort of non-dictionary, non-string IEnumerable. The following extension methods implement these checks:

    public static class JavaScriptSerializerObjectExtensions
    {
        public static bool IsJsonArray(this object obj)
        {
            if (obj is string || obj.IsJsonObject())
                return false;
            return obj is IEnumerable;
        }
    
        public static IEnumerable<object> AsJsonArray(this object obj)
        {
            if (obj is string || obj.IsJsonObject())
                return null;
            return (obj as IEnumerable).Cast<object>();
        }
    
        public static bool IsJsonObject(this object obj)
        {
            return obj is IDictionary<string, object>;
        }
    
        public static IDictionary<string, object> AsJsonObject(this object obj)
        {
            return obj as IDictionary<string, object>;
        }
    
        /// <summary>
        /// If the incoming object corresponds to a JSON array, return it.  Otherwise wrap it in an array.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static IEnumerable<object> ToJsonArray(this object obj)
        {
            if (obj.IsJsonArray())
                return obj.AsJsonArray();
            return new[] { obj };
        }
    
        public static string JsonPrimitiveToString(this object obj, bool isoDateFormat = true)
        {
            if (obj == null)
                return null; // Or return "null" if you prefer.
            else if (obj is string)
                return (string)obj;
            else if (obj.IsJsonArray() || obj.IsJsonObject())
                return new JavaScriptSerializer().Serialize(obj);
            else if (isoDateFormat && obj is DateTime)
                // Return in ISO 8601 format not idiosyncratic JavaScriptSerializer format
                // https://stackoverflow.com/questions/17301229/deserialize-iso-8601-date-time-string-to-c-sharp-datetime
                // https://msdn.microsoft.com/en-us/library/az4se3k1.aspx#Roundtrip
                return ((DateTime)obj).ToString("o");
            else
            {
                var s = new JavaScriptSerializer().Serialize(obj);
                if (s.Length > 1 && s.StartsWith("\"", StringComparison.Ordinal) && s.EndsWith("\"", StringComparison.Ordinal))
                    s = s.Substring(1, s.Length - 2);
                return s;
            }
        }
    }
    

    Then you can deserialize as follows:

    var jsonData = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(jsonString);
    
    Dictionary<string, Dictionary<string, List<Dictionary<string, string>>>> finalData;
    
    // I could have just done var finalData = ... here.  I declared finalData explicitly to makes its type explicit.
    finalData =
        jsonData.ToDictionary(
        p1 => p1.Key,
        p1 => p1.Value
            .AsJsonObject()
            .ToDictionary(
                p2 => p2.Key,
                p2 => (p2.Value.ToJsonArray().Select(a => a.AsJsonObject())).Select(o => o.ToDictionary(p3 => p3.Key, p3 => p3.Value.JsonPrimitiveToString())).ToList()
            ));
    

    Now that you have a fully typed hierarchy of dictionaries, you should be able to proceed to create your final model.