JsonPathLimits
What a single evaluation is allowed to cost, before it is abandoned.
This is a bound that refuses, never one that truncates, and the distinction is the whole design. A budget that stopped early and returned what it had would be a wrong answer with nothing to say so — which is why JsonPathQuery.forEachNodeIn still carries no such thing, and must not acquire one. Exceeding a limit here raises JsonPathLimitExceededException: the caller learns that the query was too expensive, rather than quietly receiving fewer nodes than the document holds.
What this is for. JsonPathQuery.compile already refuses a query that is too large — 1024 selectors, 64 levels of nesting — and the I-Regexp matcher is a Thompson construction precisely because RFC 9535 lets a match pattern come from the document. Neither bounds what a small, valid query costs against a large document: $..*..* is eleven characters and is quadratic in the document's node count, and a filter applied to every node of a descendant expansion is the same shape. Where the expression is supplied by someone you do not trust — which is this module's chosen use case — that gap is the whole attack.
Counted in steps, never on a clock, for the reason the regex bound is: a wall-clock budget makes the failure depend on the machine, so the same query would be rejected on a loaded CI runner and accepted on a developer's laptop. Every number here is a count of work the evaluator does.
The defaults are a backstop, not a policy. They are set so that no honest query over a document this engine can hold will meet them — the module's own fixtures walk a 20 000-deep document and a 5 000-wide array well inside them — which means a deployment that actually runs hostile expressions should set its own, far tighter, and size them against the documents it holds. NONE turns them off for a caller who has established trust some other way.
// A public endpoint compiling whatever it is handed.
val limits = JsonPathLimits(maxNodesVisited = 50_000, maxNodesProduced = 1_000, maxDescendantDepth = 32)
val query = JsonPathQuery.compile(untrusted, limits)
val nodes = try {
query.nodesIn(document)
} catch (rejected: JsonPathLimitExceededException) {
respondTooExpensive(rejected.limit) // never a partial nodelist
}Immutable, and safe to share: the limits live on the query, the counters do not. Each call to forEachNodeIn or nodesIn starts its own, which is what keeps one instance applicable to any number of documents from any number of threads at once.
Properties
levels a .. expansion may descend below the node it started at. Bounds the location chain each node carries, and with it the cost of naming one. 0 or less means no bound.
nodes the caller's sink may be handed. A query whose answer is genuinely enormous is refused rather than delivered, which is what a caller materialising with nodesIn needs. 0 or less means no bound.
node-touches allowed in one evaluation, across the whole query including the sub-walks a filter runs. Not distinct nodes: a node reached twice by two segments costs twice, because this bounds work and work is what an attacker buys. 0 or less means no bound.