Search code examples
jsonlinqc#-4.0json.netlinq-to-json

LINQ to JSON - NullReferenceException error from a JToken that could be either a JValue or JArray


I'm trying to select all users with a roleId of 4 based on the first element of the array or value. How can I do that? Also, how can I display the roleId? Here's my JSON string:

{
    "?xml" : {
        "@version" : "1.0",
        "@encoding" : "UTF-8"
    },
    "DataFeed" : {
        "@FeedName" : "AdminData",
        "People" : [{
                "id" : "63",
                "active": "1",
                "firstName" : "Joe",
                "lastName" : "Schmoe",
                "roleIds" : {
                    "int" : "4"
                }
            } , {
                "id" : "65",
                "active": "1",
                "firstName" : "Steve",
                "lastName" : "Jobs",
                "roleIds" : {
                    "int" : ["4", "16", "25", "20", "21", "22", "17", "23", "18"]
                }
            } , {
                "id" : "66",
                "active": "1",
                "firstName" : "Bill",
                "lastName" : "Gates",
                "roleIds" : {
                    "int" : ["3", "16", "25", "20"]
                }
            }
        ]
    }
}

Here's the query that I'm using:

JObject jsonFeed = JObject.Parse(jsonString);

from people in jsonFeed.SelectTokens("DataFeed.People").SelectMany(i => i)
let ids = people["roleIds.int"]
where (int) people["active"] == 1 &&
    (ids.Type == JTokenType.Array) ?
        ((int[]) ids.ToObject(typeof(int[]))).Any(k => k == 4) : 
// <-- this looks through all items in the array.  
// I just want to compare against the 1st element
        (int) ids == 4
select new {
    Id = (int) people["id"],
    ResAnFName = (string) people["firstName"],
    ResAnLName = (string) people["lastName"],
    RoleId = ?? // <-- how do I get the roleID displayed
});

I'm getting the following error on ((int[]) ids.ToObject(typeof(int[]))).Any(k => k == 4):

NullReferenceException: Object reference not set to an instance of an object.

In the end, my results should be: Joe Schmoe & Steve Jobs, only.

What am I doing wrong?


Solution

  • You need to do

    let ids = people.SelectToken("roleIds.int")
    

    Rather than

    let ids = people["roleIds.int"]
    

    That's because the ids property is nested inside the roleIds property, and for queries of nested objects, SelectToken() should be used. JObject.Item(String) only returns a property with that exact name -- which might include a .. I.e. your original let statement would work on the following:

    {
        "roleIds.int": "4"
    }
    

    While SelectToken() must be used for:

    {
        "roleIds" : {
            "int" : "4"
        }
    }
    

    The full query:

    var query = from people in jsonFeed.SelectTokens("DataFeed.People")
                       .SelectMany(i => i)
                let ids = people.SelectToken("roleIds.int")
                where (int)people["active"] == 1 &&
                    (ids.Type == JTokenType.Array) ?
                        ((int[])ids.ToObject(typeof(int[]))).Any(k => k == 4) :
                        (int)ids == 4
                select new
                {
                    Id = (int)people["id"],
                    ResAnFName = (string)people["firstName"],
                    ResAnLName = (string)people["lastName"]
                };
    

    Working fiddle.

    Update

    If you want to add RoleIds as an integer array to your returned anonymous type, you could do something like:

    int desiredRoleId = 4;
    var query = from people in jsonFeed.SelectTokens("DataFeed.People")
                       .SelectMany(i => i)
                let ids = people
                    .SelectToken("roleIds.int")
                    .SingleOrMultiple()
                    .Select(t => (int)t)
                    .ToArray()
                where (int?)people["active"] == 1 && ids.Contains(desiredRoleId)
                select new
                {
                    Id = (int)people["id"],
                    RoleIds = ids,
                    ResAnFName = (string)people["firstName"],
                    ResAnLName = (string)people["lastName"]
                };
    

    Using the extension method:

    public static class JsonExtensions
    {
        public static IEnumerable<JToken> SingleOrMultiple(this JToken source)
        {
            if (source == null)
                return Enumerable.Empty<JToken>();
            IEnumerable<JToken> arr = source as JArray;
            return arr ?? new[] { source };
        }
    }
    

    And to get the first role ID, change the select clause to:

                select new
                {
                    Id = (int)people["id"],
                    FirstRoleId = ids.FirstOrDefault(),
                    ResAnFName = (string)people["firstName"],
                    ResAnLName = (string)people["lastName"]
                };
    

    Sample fiddle.