Class JsonPathParser

java.lang.Object
ca.marcusdunn.jsonlens.path.parser.JsonPathParser

public final class JsonPathParser extends Object

Makes a JsonPathQuery from query text (RFC 9535).

The parser checks that the query is well-formed (it agrees with the ABNF grammar) and valid (its integers are in the I-JSON range and its function expressions are well-typed). It reports the first problem as a ParseError. It never throws an exception for a non-null argument.

A parser is immutable and safe for use by more than one thread. Keep one instance and use it again.

Parse a query

JsonPathParser parser = JsonPathParser.standard(); // immutable and thread-safe

Result<JsonPathQuery, ParseError> result = parser.parse("$.store.book[0].title");
String text = result.map(JsonPathQuery::toString).orElse("not valid");
// text: "$['store']['book'][0]['title']" (shorthand forms become bracket notation)

Handle errors

ParseError is a sealed interface of records. Each error has a position and a message:

String describe = switch (JsonPathParser.standard().parse("$[?length(@.*) > 3]")) {
    case Result.Ok(JsonPathQuery query) -> "valid: " + query;
    case Result.Err(ParseError.NonSingularQuery(int position)) ->
            "@.* can select more than one node, at position " + position;
    case Result.Err(ParseError.UnknownFunction(int position, String name)) ->
            "no function " + name;
    case Result.Err(ParseError error) -> error.message(); // all other errors
};
// describe: "@.* can select more than one node, at position 10"
ParseError error = JsonPathParser.standard().parse("$.a[01]").fold(query -> null, e -> e);
String message = error.message();
// "The integer '01' is not valid. An integer must not have a leading zero or be '-0'. Position: 4."

What the parser checks

Rule Example of an error Error
The grammar of RFC 9535 $.a., $[?@.a = 1], $ ParseError.UnexpectedCharacter, ParseError.UnexpectedEnd
Integers in the I-JSON range $[9007199254740992] ParseError.IntegerOutOfRange
Integers without leading zeros $[01], $[-0] ParseError.InvalidInteger
Only known functions $[?foo(@)] ParseError.UnknownFunction
Well-typed function calls $[?length(@.*) > 1], $[?count(@) == true && match(@)] ParseError.NonSingularQuery, ParseError.WrongArgumentCount, ParseError.ArgumentTypeMismatch, ParseError.NotComparable, ParseError.ValueTypeInTest
Unicode scalar values only a string with an unpaired surrogate ParseError.UnpairedSurrogate
A limit on nesting 300 nested parentheses ParseError.NestingTooDeep

Function extensions

A parser from withFunctions(List) also knows the function extensions that you give. See FunctionSignature.

  • Field Details

    • MAX_NESTING_DEPTH

      public static final int MAX_NESTING_DEPTH

      The maximum number of nested filter selectors, parentheses, and function calls.

      A deeper query gives ParseError.NestingTooDeep. The limit prevents a stack overflow from a query that an attacker makes (RFC 9535, Section 4.1).

      See Also:
  • Method Details

    • standard

      public static JsonPathParser standard()
      Returns a parser that knows the standard functions: length, count, match, search, and value.
      Returns:
      a parser for the standard functions
    • withFunctions

      public static Result<JsonPathParser, FunctionRegistrationError> withFunctions(List<FunctionSignature> extensions)

      Returns a parser that knows the standard functions and more function extensions.

      The evaluator that applies the queries must have an implementation for each signature.

      // The parser needs the signature, to check that queries are well-typed.
      Result<JsonPathParser, FunctionRegistrationError> parser =
              JsonPathParser.withFunctions(List.of(First.SIGNATURE));
      // The evaluator needs the implementation.
      Result<JsonPathEvaluator, ExtensionError> evaluator =
              JsonPathEvaluator.withFunctions(List.of(new First()));
      
      JsonPathQuery query = parser.orElse(JsonPathParser.standard())
              .parse("$[?first(@.*) > 4]")
              .orElse(null);
      List<Node<JsonNode>> nodes = evaluator.orElse(JsonPathEvaluator.standard())
              .evaluate(query, root, JacksonJsonModel.INSTANCE)
              .orElse(List.of());
      // nodes: the path $[1]
      

      A name that is not valid gives FunctionRegistrationError.InvalidName. A name that is used more than once, or that is the name of a standard function, gives FunctionRegistrationError.DuplicateName:

      Result<JsonPathParser, FunctionRegistrationError> clash = JsonPathParser.withFunctions(List.of(
              new FunctionSignature("length", FunctionType.VALUE, List.of(FunctionType.VALUE))));
      // clash: Err(DuplicateName[name=length]). A standard function cannot be replaced.
      
      Parameters:
      extensions - the signatures of the function extensions
      Returns:
      the parser, or an error if a name is not valid or is used more than once
    • functions

      public List<FunctionSignature> functions()
      Returns the functions that this parser knows.
      Returns:
      the function signatures, the standard functions first
    • parse

      public Result<JsonPathQuery, ParseError> parse(String query)

      Parses query text.

      The text must be the complete query. Blank space before $ or after the last segment is not permitted. Shorthand forms become their bracket equivalents:

      JsonPathParser parser = JsonPathParser.standard(); // immutable and thread-safe
      
      Result<JsonPathQuery, ParseError> result = parser.parse("$.store.book[0].title");
      String text = result.map(JsonPathQuery::toString).orElse("not valid");
      // text: "$['store']['book'][0]['title']" (shorthand forms become bracket notation)
      
      Parameters:
      query - the query text
      Returns:
      the query, or the first error
    • parse

      public Result<JsonPathQuery, ParseError> parse(byte[] utf8)

      Parses query text in UTF-8 (RFC 9535, Section 2.1).

      Use this method for a query that comes from a network or a file. The decoder does not replace bytes that are not valid. It gives an error with the position of the first such byte:

      byte[] utf8 = "$['café']".getBytes(StandardCharsets.UTF_8);
      Result<JsonPathQuery, ParseError> fromBytes = JsonPathParser.standard().parse(utf8);
      
      byte[] broken = {'$', '[', '\'', (byte) 0xC3, '(', '\'', ']'};
      Result<JsonPathQuery, ParseError> rejected = JsonPathParser.standard().parse(broken);
      // rejected: Err(InvalidUtf8[position=3])
      
      Parameters:
      utf8 - the query text in UTF-8
      Returns:
      the query, or the first error. Bytes that are not well-formed UTF-8 give ParseError.InvalidUtf8.