13

Using the SelectToken method of JSON.NET to select a token with JSONPath, I found no way to specifiy that the search should be case-insensitive.

E.g.

json.SelectToken("$.maxAppVersion")

should return a matching token, no matter whether it is written maxappversion, MAXAPPVERSION or any other casing.

My question:

Is there an official way or at least a work-around to use JSONPath in a case-insensitive way?

(The closest I found was this similar question for a Java implementation of JSON)

Community
  • 1
  • 1
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291

6 Answers6

11

This is not implemented in Json.NET as of version 8.0.2.

JSONPath property name matching is done with two classes: FieldFilter for simple name matches, and ScanFilter for recursive searches. FieldFilter has the following code, where o is a JObject:

JToken v = o[Name];
if (v != null)
{
    yield return v;
}

Internally JObject uses a JPropertyKeyedCollection to hold its properties, which in turn uses the following comparer for property name lookups:

private static readonly IEqualityComparer<string> Comparer = StringComparer.Ordinal;

It is thus case-sensitive. Similarly, ScanFilter has:

JProperty e = value as JProperty;
if (e != null)
{
    if (e.Name == Name)
    {
        yield return e.Value;
    }
}

Which is also case sensitive.

There's no mention of case-insensitive matching in the JSONPath standard so I think what you want simply isn't available out of the box.

As a workaround, you could add your own extension methods for this:

public static class JsonExtensions
{
    public static IEnumerable<JToken> CaseSelectPropertyValues(this JToken token, string name)
    {
        var obj = token as JObject;
        if (obj == null)
            yield break;
        foreach (var property in obj.Properties())
        {
            if (name == null)
                yield return property.Value;
            else if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
                yield return property.Value;
        }
    }

    public static IEnumerable<JToken> CaseSelectPropertyValues(this IEnumerable<JToken> tokens, string name)
    {
        if (tokens == null)
            throw new ArgumentNullException();
        return tokens.SelectMany(t => t.CaseSelectPropertyValues(name));
    }
}

And then chain them together with standard SelectTokens calls, for instance:

var root = new { Array = new object[] { new { maxAppVersion = "1" }, new { MaxAppVersion = "2" } } };

var json = JToken.FromObject(root);

var tokens = json.SelectTokens("Array[*]").CaseSelectPropertyValues("maxappversion").ToList();
if (tokens.Count != 2)
    throw new InvalidOperationException(); // No exception thrown

(Relatedly, see the Json.NET issue Provide a way to do case-sensitive property deserialization which requests a case-sensitive contract resolver for consistency with the case-sensitivity of LINQ-to-JSON.)

Pooven
  • 1,744
  • 1
  • 25
  • 44
dbc
  • 104,963
  • 20
  • 228
  • 340
  • It looks like your extension method only goes one level deep, which doesn't solve the `SelectToken` problem. Does your extension method have an advantage over `GetValue`? – Kyle Delaney Feb 25 '20 at 01:35
  • 2
    @KyleDelaney - been a long time since I answered this, but [`GetValue(String, StringComparison)`](https://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Linq_JObject_GetValue_1.htm) returns a **single** value whereas the extension method returns multiple values, which may be useful in cases where there are multiple properties that differ only by case. – dbc Feb 26 '20 at 22:44
4

It's really surprising that Newtonsoft gets away without supporting this. I had to write a custom JToken extension to support this. I did not need the entire JSONPath, needed just a few basic path queries. Below is the code I used

public static JToken GetPropertyFromPath(this JToken token, string path)
{
    if (token == null)
    {
        return null;
    }
    string[] pathParts = path.Split(".");
    JToken current = token;
    foreach (string part in pathParts)
    {
        current = current.GetProperty(part);
        if (current == null)
        {
            return null;
        }
    }
    return current;
}

public static JToken GetProperty(this JToken token, string name)
{
    if (token == null)
    {
        return null;
    }
    var obj = token as JObject;
    JToken match;
    if (obj.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out match))
    {
        return match;
    }
    return null;
}

With the above code I can parse JSON as follows

var obj = JObject.Parse(someJson);
JToken tok1 = obj.GetPropertyFromPath("l1.l2.l3.name"); // No $, or other json path cliché
JToken tok2 = obj.GetProperty("name");
string name = obj.StringValue("name"); // Code in the link below

Code to entire extension available here

rjv
  • 6,058
  • 5
  • 27
  • 49
2

When I want to get a token and not have to worry about case I do this:

var data = JObject.Parse(message.Data);
var dataDictionary = new Dictionary<string, JToken>(data.ToObject<IDictionary<string, JToken>>(),
                                                        StringComparer.CurrentCultureIgnoreCase);

If there are any nested structures I need to worry about then I have to do it again for those but that StringComparer means either dataDictionary["CampaignId"].ToString(); or dataDictionary["campaignId"].ToString(); will work and I get both.

onesixtyfourth
  • 744
  • 9
  • 30
1

What worked for me is converting everything to uppercase then search in the uppercase string, but the value is returned from the original string:

public static string SelectValue(this string jsonString, string jsonPath)
{
    string result = null;

    string jsonStringToUpper = jsonString.ToUpper();
    string jsonPathToUpper = jsonPath.ToUpper();

    var jsonObj = JObject.Parse(jsonStringToUpper);
    string valueToUpper = (string)jsonObj.SelectToken(jsonPathToUpper);

    if (!string.IsNullOrEmpty(valueToUpper))
    {
        int upperCaseIndex = jsonStringToUpper.IndexOf(valueToUpper);

        result = jsonString.Substring(upperCaseIndex, valueToUpper.Length);
    }

    return result;
}
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
KrisSki
  • 11
  • 1
  • Nice one! Probably better use [`ToUpperInvariant`](https://stackoverflow.com/q/3550213/107625), I guess. – Uwe Keim Apr 23 '21 at 12:13
  • 1
    This has issues, if there are multiple objects with same property value, the IndexOf will always grab the first one. Also, if the object contains null, true, false, it'll fail in the JObject.Parse method. Those MUST be converted to small letters. This should work for a simple object with no arrays that have similar properties/values. – Desolator Jul 21 '21 at 19:13
1

A quick dirty hackish solution is to convert the object keys to upper-case then perform the JsonPath query, a simple example for your case (just to give you an idea). To include in-depth search then you must convert all of the keys in the child objects (requires recursion):

var path = "maxappversion";
var json = "{\"maxAppVersion\": 1}";
var type = typeof(Dictionary<string, int>);

// This will convert the json str to actual Dictionary object
var jsonObj = JsonSerializer.Deserialize(json, type); 

// Loop through all of the properties in the dictionary and convert the keys to uppercase
foreach (var kpv in jsonObj) jsonObj[kpv.Key.ToUpperCase()] = kpv.Value; 

// Look for the path
try
{
    // This will return list of tokens, if not found, exception will be thrown
    var tokens = JObject.FromObject(jsonObj).SelectTokens(path.ToUpperCase(), true);
    var value = (int)tokens.FirstOrDefault(); // Use linq or just loop 
}
catch (JsonException)
{
    // PathNotFound
}

Please note that this code isn't tested.

Update: For faster key conversion, you can do a regex replace to replace all of the keys in the json string. The pattern would be something like: "\"([^\"]+)\"\s*:"

Desolator
  • 22,411
  • 20
  • 73
  • 96
1

I took inspiration from this answer and made all keys lowercase first. If you also makes your JSONPath lowercase it will be case-insensitive. This works well if you are creating the JObject using JObject.FromObject.

First create a NamingStrategy

public class LowercaseNamingStrategy : NamingStrategy
{
    protected override string ResolvePropertyName(string name)
    {
        return name.ToLowerInvariant();
    }
}

Then use it to create your JObject

var jObject = JObject.FromObject(
    anObject,
    new JsonSerializer
    {
        ContractResolver = new DefaultContractResolver
        {
            NamingStrategy = new LowercaseNamingStrategy()
        }
    }
);
var value = jObject.SelectToken(path.ToLowerInvariant())
Fredrik Wallén
  • 438
  • 5
  • 13