JsonPointer from Linq Expression

517 Views Asked by At

Is there a way to get the JsonPointer of a given Linq.Expression as it would be serialized by a given Newtonsoft.Json contract resolver?

e.g.

public class Foo { public string Bar { get; set; } }
var path = GetJsonPointer<Foo>(x => x.Bar, new CamelCasePropertyNamesContractResolver())
//how to write GetJsonPointer so that "path" would equal "/bar"?
2

There are 2 best solutions below

2
Arithmomaniac On BEST ANSWER

The trickiest part here is to get the name of a property as it's going to be serialized. You can do that in the following way:

static string GetNameUnderContract(IContractResolver resolver, MemberInfo member)
{
    var contract = (JsonObjectContract)resolver.ResolveContract(member.DeclaringType);
    var property = contract.Properties.Single(x => x.UnderlyingName == member.Name);
    return property.PropertyName;
}

Once you have that, you can just handle each level of the expression and pop the result onto a string stack. The following quick-and-dirty implementation supports indexing in addition to simple member access.

public string GetJsonPointer<T>(IContractResolver resolver, Expression<Func<T,object>> expression)
{
    Stack<string> pathParts = new();

    var currentExpression = expression.Body;
    while (currentExpression is not ParameterExpression)
    {
        if (currentExpression is MemberExpression memberExpression)
        {
            // Member access: fetch serialized name and pop
            pathParts.Push(GetNameUnderContract(memberExpression.Member));
            currentExpression = memberExpression.Expression;
        }
        else if (
            currentExpression is BinaryExpression binaryExpression and { NodeType: ExpressionType.ArrayIndex }
            && binaryExpression.Right is ConstantExpression arrayIndexConstantExpression
        )
        {
            // Array index
            pathParts.Push(arrayIndexConstantExpression.Value.ToString());
            currentExpression = binaryExpression.Left;
        }
        else if (
            currentExpression is MethodCallExpression callExpression and { Arguments: { Count: 1 }, Method: { Name: "get_Item" } }
            && callExpression.Arguments[0] is ConstantExpression listIndexConstantExpression and { Type: { Name: nameof(System.Int32) } }
            && callExpression.Method.DeclaringType.GetInterfaces().Any(i=>i. IsGenericType && i.GetGenericTypeDefinition()==typeof(IReadOnlyList<>))
        )
        {
            // IReadOnlyList index of other type
            pathParts.Push(listIndexConstantExpression.Value);
            currentExpression = callExpression.Object;
        }
        else
        {
            throw new InvalidOperationException($"{currentExpression.GetType().Name} (at {currentExpression}) not supported");
        }
    }

    return string.Join("/", pathParts);
}

Example of invocation:

public record Foo([property: JsonProperty("Barrrs")] Bar[] Bars);
public record Bar(string Baz);

var resolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() };
GetJsonPointer<Foo>(resolver, x => x.Bars[0].Baz).Dump();
//dumps "/Barrrs[0]/baz"
6
gregsdennis On

Edit:

For json pointer, check out JsonPointer.Net. It's part of the new suite and operates on System.Text.Json.

Since your wanting a pointer, it doesn't matter that you're using Newtonsoft. Produce the pointer with linq, call .ToString(), then you can use the result with Newtonsoft.

// get a pointer
var pointer = JsonPointer.Create<Foo>(x => x.Bar);
var asString = pointer.ToString();

// use the pointer

It doesn't support custom casing right now; it uses the casing of the model. But that's a good feature idea I could add fairly simply. It would use the System.Text.Json mechanisms instead of Newtonsoft.

See the docs for more info.


For json path:

Have a look at Manatee.Json. It can definitely do that.

It's the precursor to my json-everything suite.

It's deprecated now in favor of my new libraries, but I haven't added this particular feature to the new library yet.