TL;DR
JMESPath is a powerful query language that enables processing of JSON payloads. It can be used in .NET, see JmesPath.Net.
Source code: https://github.com/NikiforovAll/jmespath-demo
Introduction
JSON processing is a common task in the day-to-day work of developers. We are used to working with JSON, but, occasionally, we need something more dynamic and efficient than System.Text.Json
and Newtonsoft.Json
. JMESPath is a powerful query language that allows you to perform Map/Reduce tasks in a declarative and intuitive manner.
JMESPath is simple to use, the query itself is just a plain string. The benefit of this approach is that you can follow the inversion of control principle and give your users the control of writing JMESPath queries.
💡 For example, the Azure CLI uses the –query parameter to execute a JMESPath query on the results of commands.
public sealed class JmesPath
{
public string Transform(string json, string expression);
}
Demo
Read a random example of JSON string from a file:
var source = new StreamReader("./example.json").ReadToEnd();
The content of the file:
{
"_id": "63ba60670fe420f2fb346866",
"isActive": true,
"balance": "$2,285.51",
"age": 20,
"eyeColor": "blue",
"name": "Eva Sharpe",
"email": "evasharpe@zaggles.com",
"phone": "+1 (950) 479-2130",
"registered": "2023-01-08T08:07:44.1787922+00:00",
"latitude": 46.325291,
"longitude": 5.211461,
"friends": [
{
"id": 0,
"name": "Nielsen Casey",
"age": 19
},
{
"id": 1,
"name": "Carlene Long",
"age": 38
}
]
}
The code below shows the processing of the example payload above. It demonstrates different concepts such as projections, filtering, aggregation, type transformation, etc. I think the syntax is quite intuitive and doesn’t need an explanation.
The processing is quite simple:
var expressions = new (string, string)[]
{
("scalar", "balance"),
("projection", "{email: email, name: name}"),
("functions", "to_string(latitude)"),
("arrays", "friends[*].name"),
("filtering", "friends[?age > `20`].name"),
("aggregation", "{sum: sum(friends[*].age), names: join(',', friends[*].name)}"),
("now. ISO 8601", "now()"),
("now. Universal sortable date/time pattern", "now('u')"),
("now. Long date pattern", "now('D')"),
("format", "date_format(registered, 'd')"),
};
foreach (var (exampleName, expression) in expressions)
{
var result = parser.Transform(source, expression);
}
Extensibility
The cool thing about JMESPath is it provides a way to add custom functions. See more details: https://github.com/jdevillard/JmesPath.Net/issues/81
For example, here is how we can write now()
function that accepts .NET string format provider, see the Microsoft docs https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings.
public class NowFunction : JmesPathFunction
{
private const string DefaultDateFormat = "o";
public NowFunction() : base("now", minCount: 0, variadic: true) { }
public override JToken Execute(params JmesPathFunctionArgument[] args)
{
var format = args is { Length: > 0 } x
? x[0].Token
: DefaultDateFormat;
return new JValue(DateTimeOffset.UtcNow.ToString(format.ToString()));
}
}
Summary
As you can see, JMESPath solves the issues of dynamic JSON processing based on user input quite nicely. It has an extensibility model that opens tons of possibilities.