Home JsonPath.Net Basics
JsonPath.Net Basics
Cancel

JsonPath.Net Basics

JSON Path is a query language for JSON documents inspired by what XPath provides for XML documents. It was originally proposed by Stefan Gössner and is now an IETF specification: RFC 9535.

The library was promoted to version with the release of the specification, and also includes additional support for the following features, which the specification team passed on:

  • arithmetic operations in expressions
  • in operator in expressions
  • starting a path with @ instead of $ (relative paths)
  • object and arrays in expressions (does not support single quotes)

Syntax

A path consists of start indicator $ followed by a series of segments, chained one after another. Each segment contains one or more selectors. Each selector takes in a collection of JSON nodes and produces a collection of JSON nodes (called a “nodelist”) based on their function. The output of the segment is the collective output of all of that segment’s selectors. Each segment takes as input the output of the previous selector.

  • $ - This is the root. A path must always begin with it. It effectively “selects” the root document. It can also be used in query expressions, which we’ll come to later.
  • [...] - Square brackets indicate a segment. Selectors are found between the brackets, separated by commas. There are several kinds of selectors:
    • Index - The basic integer index that we’re all familiar with. Negative numbers will start counting from the end of an array. If the value is out of the range of the array, no nodes will be returned.
    • Slice - This allows selection of a range of nodes from an array. Again, this range is clamped to the bounds of the array.
    • Property name - This allows property selection of objects by matching the full name. Names can be single- or double-quoted and special characters must be encoded as is required by JSON.
    • Wildcard (* literal) - This simply returns all values from within an object or array.
    • Filter - This is an expression that evaluates to a boolean and operates on the child nodes of the node being passed to the selector. It is denoted by a question mark followed an expression ?... that returns a boolean result. (see note below)
  • .. - This is a recursive descent operator. It is not itself a segment, but a segment prepended with this operator will recursively query the entire subtree rather than just the local value.

This boolean result is distinct from a JSON boolean, which is denoted by either the true or false literals. Using true or false in an expression is interpreted as a JSON literal and must be used in a comparison.

In addition to the above, there are a few shorthand options for some special cases. These are only valid when the segment contains only a single selector and that selector is either a property name or a wildcard.

  • ['foo'] may be rewritten as .foo.
  • [*] may be rewritten as .*.
  • ..['foo'] may be rewritten as ..foo
  • ..[*] may be rewritten as ..*

Query Expressions

Filter selectors take an expression. This expression uses a single variable, which is a JSON node as described above. The node is denoted by @ and any valid JSON Path can follow it. The @ is a stand-in for the $ from above and acts as the root of the local value.

For example, an item query expression may be ?@.price<10. This expression will find all items in either an object or an array that contains a price property with a value less than 10.

Additionally, the $ selector may also be used to reference back to the root node. This allows queries like ?@.price<$.maxPrice where we want to find all of the items of the current container that contain a price value that is less than the value in the root node’s maxPrice property.

This library considers paths that start with $ to be “globally scoped” and paths that start with @ to be “locally scoped.”

Expressions support the following operations:

  • Arithmetic
    • +
    • -
    • *
    • /
  • Comparison
    • ==
    • !=
    • <
    • <=
    • >
    • >=
  • Boolean logic
    • &&
    • ||

Arithmetic operations are not part of the specification (yet), and cannot be expected to work in other JSON Path implementations.

Functions

There is also support for functions within query expressions, which works as an extension point to add your own custom logic.

A function is a name followed by a parentheses containing zero or more parameters separated by commas. Parameters can be JSON literals, JSON Paths (global or local), or even other functions. (That is, the return value of function calls can be parameters, e.g. min(max(@,0),10); passing one function into another isn’t supported.)

The specification defines the following functions:

  • length(<value>) to return the length of a string or the number of items in an array or object. Takes a single parameter.
  • count(<nodelist>) to return the number of nodes in a nodelist.
  • match(<text>, <iregexp>) to return whether the text is an exact match for a regular expression per I-Regexp. (see note below)
  • search(<text>, <iregexp>) to return whether the text contains an exact match for a regular expression per I-Regexp. (see note below)
  • value(<nodelist>) to convert a nodelist to a value: single-nodelists extract their node’s value; multiple-/empty nodelists convert to Nothing (no node, absent).

I-Regexp is designed to be an interoperable subset of most popular regular expression specifications and implementations. One difference that could not be resolved was implicit anchoring. As such, two methods were developed to handle both cases. match uses implicit anchoring, while search does not.

Custom Functions

There is also support for defining your own functions.

To do this, you’ll need to decide what type your function will return and derive from the appropriate base class:

Return typeWhat that meansBase classAssociated C# type
ValueTypeJSON values or NothingValueFunctionDefinitionJsonNode?
LogicalTypea boolean (not related to the JSON true/false literals)LiteralFunctionDefinitionbool
NodesTypea nodelist, as returned by @.a or @.*NodeListFunctionDefinitionNodeList

Once you’ve created your function class, you’ll need to implement the Name property (abstracted in the base class) to identify the function. You’ll also need to create an Evaluate() function that

  • returns the associated C# type for the base class
  • only has parameters of one of the above types

You’re welcome to have as many parameters as you want, as long as they’re one of the three types listed in the table.

For an example, please see the LengthFunction implementation in the code.

Now that your function class is created, all that’s left is to register it:

1
FunctionRepository.Register(new MyCustomFunc());

In Code

To obtain an instance of a JSON Path, you’ll need to parse it from a string.

1
var path = JsonPath.Parse("$.prop[0:6:2]");

or

1
var success = JsonPath.TryParse("$.prop[0:6:2]", out JsonPath path);

This will create a JsonPath instance that will select the prop property of an object, and subsequently items, 0, 2, and 4 of an array that resides there.

Once your path is created, you can start evaluating instances.

1
2
3
var instance = JsonNode.Parse("{\"prop\":[0,1,2,3]}");

var results = path.Evaluate(instance);

This will return a results object that contains the resulting nodelist or an error.

A node contains both the value that was found and the location in the instance where it was found. The location is always represented using the “canonical,” bracketed format.

Deferred Execution

The nodelist is an IEnumerable<Node> and works like any other Linq query. That is, it is a deferred execution query, meaning that it is not actually evaluated until you request results. Additionally, if the dataset changes, and you request results again without changing the query, you’ll get different results based on the new dataset.

This is evidenced in this test:

1
2
3
4
5
6
7
8
9
10
var data = new JsonArray { "bob", "sam", "alice" };
var path = JsonPath.Parse("$[? length(@) > 3 ]");

var result = path.Evaluate(data);

Assert.AreEqual(1, result.Matches!.Count);

data[0] = "sally";

Assert.AreEqual(2, result.Matches!.Count);

Here, the query is looking for values longer than three chars. In the first data set, there is only one, but when the data set is changed, without having to re-evaluate the path, there are two results.

Adherence to the Proposed Specification

As the specification is still under authorship, there are features present in traditional JSON Path that haven’t been properly described yet. For these features, this library has been configured to mimic the consensus behaviors of other libraries as determined by the JSON Path Comparison project.

There are also a few other features of traditional JSON Path that the specification has explicitly elected not to support, such as container expressions (e.g. $[(@.length-1)]). This library will strive to prioritize the specification over the comparison consensus where any conflict exists.

Ahead of Time (AOT) compatibility

JsonPath.Net v0.8.x includes updates to support Native AOT applications. However because everything in this library is handled via parsing and direct-to-string output, you don’t need to do anything.

You’re done. Congratulations.

Contents