jetro
Jetro is a library for querying, transforming, and comparing JSON. It provides a compact expression language, a bytecode VM with caching.
Jetro has minimal dependencies and operates on top of serde_json.
This project is in experimental phase — significant changes may occur.
Quick start
use Jetro;
use json;
let j = new;
let titles = j.collect.unwrap;
assert_eq!;
One-shot evaluation (tree-walker, no VM caching):
let result = query.unwrap;
Rust API tour
Progressive walk through the Rust API over one rich payload — simple field access through compile-time macros. Every snippet runs against this document:
use *;
let doc = json!;
let j = new;
Jetro holds a thread-local VM; collect() compiles each unique expression once (then reuses the bytecode + pointer cache), so rerunning the same or similar query is essentially free.
1. Field access
assert_eq!;
assert_eq!;
assert_eq!;
2. Filter + map
let cheap = j.collect?;
// ["Dune", "Foundation", "The Hobbit"]
3. Aggregations
let in_stock_total = j.collect?;
let mean_price = j.collect?;
let well_rated = j.collect?;
4. Shaped projections
Build a new object per element:
let summary = j.collect?;
5. Sort, group, membership
let scifi_by_year = j.collect?;
let by_first_tag = j.collect?;
6. Comprehensions
let out_of_stock = j.collect?;
// ["Foundation"]
let id_to_title = j.collect?;
7. Let bindings + f-strings
let headline = j.collect?;
// "Top-rated: The Hobbit (4.83)"
8. Pipelines
| passes the left value through the right expression as @:
let avg = j.collect?;
let shout = j.collect?; // "DUNE"
9. Search — shallow and deep
Shallow search / transform helpers (v2.2):
// .find / .find_all — aliases of .filter
let hit = j.collect?;
let classic = j.collect?;
// .pick — field list with optional rename
let cards = j.collect?;
// .unique_by — dedup by derived key
let first_per_author = j.collect?;
// .collect — scalar → [scalar], array → identity, null → []
let always_array = j.collect?;
Deep search walks every descendant (DFS pre-order):
// $..find(pred) — every descendant satisfying pred
let cheap_everywhere = j.collect?;
// $..shape({k, …}) — every object that has all listed keys
let books_like = j.collect?;
// $..like({k: v}) — every object whose listed keys equal the literals
let paid_orders = j.collect?;
.deep_find / .deep_shape / .deep_like are the method-form aliases.
10. Chain-style writes
Rooted $.<path>.<op>(...) chains desugar into a single patch block — terse mutation without leaving the query language:
// .set — replace value at path
let bumped = j.collect?;
// .modify — rewrite using @ bound to current leaf
let discounted = j.collect?;
// .delete — remove the leaf
let pruned = j.collect?;
// .unset(key) — drop a child of the leaf object
let anon = j.collect?;
Breaking change vs v1: $.field.set(v) now returns the full doc with the value written back (old behaviour: ignore receiver, return v). Pipe form preserves v1 semantics — $.field | set(v) returns v — so inside .map(...) you can still use the pipe form.
11. Conditional — Python-style ternary
let label = j.collect?;
// ["ok", "out", "ok", "ok", "ok"]
Right-associative — chains naturally without parens. Short-circuits: only the taken branch runs. A literal true / false condition folds at compile time so expensive_expr() if false else cheap_expr() never compiles the dead branch.
12. Patch blocks
In-place updates that compose like any other expression:
let discounted = j.collect?;
Patch-field syntax: path: value when predicate?. Paths start with an identifier and may include .field, [n], [*], [* if pred], and ..field. DELETE removes the matched key.
13. Custom methods
Register Rust code as a first-class method:
use ;
use ;
use EvalError;
;
let mut reg = new;
reg.register;
let p90 = query_with?;
14. Typed Expr<T>
Expressions carry a phantom return type:
let titles: = new?;
let upper: = new?;
// `|` composes expressions end-to-end
let pipeline = titles | upper;
let shouted = pipeline.eval?;
15. VM caching
Every query through Jetro::collect or VM::run_str hits two caches:
- Program cache:
expr string → Arc<[Opcode]>. Each unique expression is parsed + compiled exactly once. - Pointer cache: resolved field paths keyed by
(program_id, doc_hash). Structural queries short-circuit straight to the leaves on repeat calls.
Rerunning a workload over fresh documents of the same shape is effectively free after the first call. For one-shot queries use jetro::query(expr, &doc) — it skips the VM entirely.
16. Recursive descent + drill-down chains
$..field walks every descendant level. Combine with filters and projections:
let stats = j.collect?;
let keyed = j.collect?;
17. Cross-join via nested comprehensions
Two generators produce the cartesian product; the if filter keeps only matching pairs:
let receipts = j.collect?;
Three nested loops + three lookups folded into one compiled program. Hits the pointer cache on repeat calls.
18. Layered patch transforms
One patch block stacks path-scoped clauses. Each clause runs on the patched result of the previous one:
let restocked = j.collect?;
Two filter layers: [* if pred] filters per element against the element's own fields; when pred on a field is evaluated against root $. Chain blocks with let ... in patch ... when a later mutation needs to see earlier output.
19. Engine — shared VM across threads
Jetro gives each thread its own VM. Engine is a Send + Sync handle around a Mutex<VM> so one warm cache services many workers:
use *;
use Arc;
let engine = new; // Arc<Engine>
let shared = clone;
spawn.join.unwrap;
First call compiles and fills both caches; every subsequent thread hitting the same expression skips to execution.
20. Compile-time expressions — jetro!
Enable the macros feature. Expressions get a full pest parse at compile time, with errors pointing at the exact macro call-site. Returns a typed Expr<Value>:
use jetro;
let avg_price = jetro!;
let n_classic = jetro!;
assert_eq!;
assert_eq!;
Unbalanced parens, unterminated strings, unknown operators — build failures, not runtime errors.
21. #[derive(JetroSchema)] — attribute-driven schemas
Pair a type with a fixed set of named expressions. The derive emits EXPRS, exprs(), and names(). Each #[expr(...)] line is grammar-checked at compile time, so an invalid expression in a schema never ships.
use JetroSchema;
;
assert_eq!;
for in exprs
Feature flags
| Feature | Pulls in | Unlocks |
|---|---|---|
macros |
jetro-macros companion crate |
jetro!(...) macro, #[derive(JetroSchema)] |
Cargo.toml:
[]
= { = "0.3", = ["macros"] }
Syntax
Root and context
| Token | Meaning |
|---|---|
$ |
Document root |
@ |
Current item (lambdas, pipes, comprehensions, patch paths) |
Navigation
$.field // child field
$.a.b.c // nested
$.user?.name // null-safe (postfix `?` on .user, not prefix `?.`)
$.items[0] // array index
$.items[-1] // last
$.items[2:5] // slice [2, 5)
$.items[2:] // tail
$.items[:5] // head
$..title // recursive descent
$..services?.first() // descendant + null-safe + explicit first
Postfix ? is null-safety only. It does not take the first element of
an array — for that use .first() explicitly. ! keeps its
exactly-one-element meaning (errors on 0 or >1).
Literals
null true false
42 3.14
"hello" 'world'
f"Hello {$.user.name}!" // f-string with interpolation
f"{$.price:.2f}" // format spec
f"{$.name | upper}" // pipe transform
Operators
== != < <= > >= // comparison
~= // fuzzy match (case-insensitive substring)
+ - * / % // arithmetic
and or not // logical
$.field ?| "default" // null coalescing
Methods
All operations are dot-called methods:
$.books.filter(price > 10)
$.books.map(title)
$.books.sum(price)
$.books.filter(price > 10).count()
$.books.sort_by(price).reverse()
$.users.group_by(tier)
$.items.map({name, total: price * qty})
Search — shallow and deep
$.books.find(title == "Dune") // first match (alias of filter + [0])
$.books.find_all("classic" in tags) // alias of filter
$.books.pick(title, slug: id, year) // project + rename
$.books.unique_by(author) // dedup by key
$.books[0].tags.collect() // scalar→[scalar], arr→arr, null→[]
$..find(@.price > 10) // deep: every descendant satisfying pred
$..shape({id, title, price}) // deep: every obj with all listed keys
$..like({status: "paid"}) // deep: every obj with listed key==lit
Chain-style writes (terminals on rooted paths)
$.a.b.set(v) // replace leaf — returns full doc
$.a.b.modify(@ * 2) // replace using @ = current leaf
$.a.b.delete() // remove leaf
$.a.b.unset(k) // remove child of leaf object
Conditional (Python-style ternary)
then_ if cond else else_
"big" if n > 10 else "small"
"a" if n == 1 else "b" if n == 2 else "c" // right-associative
Comprehensions
[book.title for book in $.books if book.price > 10]
{user.id: user.name for user in $.users if user.active}
Let bindings
let top = $.books.filter(price > 100) in
{count: top.len(), titles: top.map(title)}
Lambdas
$.books.filter(lambda b: b.tags.includes("sci-fi"))
Kind checks
$.items.filter(price kind number and price > 0)
$.data.filter(deleted_at kind null)
Pipelines
| passes the left value through the right expression as @:
$.name | upper
$.price | (@ * 1.1)
Patch blocks
In-place updates with block-form syntax. Each clause matches a path and produces either a replacement value or DELETE:
patch $ {
books[*].price: @ * 1.1,
books[* if stock == 0]: DELETE,
meta.updated_at: now()
}
Architecture
Expression engine
Everything below is shipped by the jetro-core crate and re-exported here.
grammar.pest+parser.rs— pest PEG grammar →ast::Expreval/mod.rs— tree-walking evaluator (reference semantics)eval/value.rs—Valtype:Arc-wrapped compound nodes, O(1) cloneseval/func_*.rs— built-in methods grouped by domain (strings, arrays, objects, paths, aggregates, csv)eval/methods.rs—Methodtrait +MethodRegistryfor user-registered methodsvm.rs— bytecode compiler + stack machine; 10 peephole passes; compile and pointer cachesgraph.rs— multi-document query primitive (merges named nodes into a virtual root)analysis.rs/schema.rs/plan.rs/cfg.rs/ssa.rs— optional IRs (type/nullness/cardinality, shape inference, logical plan, CFG, SSA numbering)
See jetro-core/README.md for a full technical walkthrough: AST variants, opcode set, peephole passes, cache invariants.
Companion crates
jetro-core— engine (this crate re-exports it)jetro-macros—jetro!()+#[derive(JetroSchema)](feature = "macros")
License
MIT. See LICENSE.