Search code examples
c#json.netjsonpointer

JsonPointer from Linq Expression


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"?

Solution

  • 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"