A Practical Tour
This tour teaches Jetro the way you will probably use it: grab a real JSON
payload, ask a precise question, reshape the answer, and move on. Every query in
this chapter was checked with the release build of jetrocli 0.2.9.
Run a query against a JSON file:
jetrocli -e '$.services.filter(@.enabled).count()' < services.json
Run a row-local query against NDJSON:
jetrocli --ndjson -i events.ndjson -e '$.service + ":" + $.level'
The Working Document
Save this as services.json:
{
"services": [
{"name":"api","lang":"rust","latency_ms":42,"owner":"platform","enabled":true,"errors":2,"tags":["edge","json"]},
{"name":"worker","lang":"go","latency_ms":85,"owner":"data","enabled":true,"errors":9,"tags":["queue"]},
{"name":"admin","lang":"ts","latency_ms":130,"owner":"platform","enabled":false,"errors":0,"tags":["internal"]}
],
"deploys": [
{"service":"api","sha":"a1","status":"ok"},
{"service":"worker","sha":"b2","status":"fail"}
],
"meta": {"env":"prod","version":7}
}
1. Start With Paths
Use $ for the root document, then walk fields and indexes.
QUERY: $.services[0].name
OUT: "api"
Wildcards collect the same field from many array items:
QUERY: $.services[*].name
OUT: ["api","worker","admin"]
2. Filter Like You Would In Code
Inside filter, map, and similar methods, @ is the current item.
QUERY: $.services.filter(@.enabled).count()
OUT: 2
That is the basic Jetro shape: start from a path, chain operations, return the value you actually need.
3. Return A Useful Shape
Projection objects let you rename fields, drop noise, and compute small derived values in one pass.
QUERY:
$.services
.filter(@.enabled)
.map({name: @.name, p95: @.latency_ms, owner: @.owner})
OUT:
[
{"name":"api","owner":"platform","p95":42},
{"name":"worker","owner":"data","p95":85}
]
This is where Jetro starts paying rent in developer workflows: the output is already shaped for the next command, dashboard, test assertion, or API boundary.
4. Sort, Bound, Then Project
Use sort_by, take, and map for top-N questions.
QUERY:
$.services
.filter(@.enabled)
.sort_by(-latency_ms)
.take(1)
.map({service: name, alert: errors > 5})
OUT:
[
{"alert":true,"service":"worker"}
]
The minus sign sorts descending by latency. take(1) makes the intended demand
explicit: you only want the worst enabled service.
5. Aggregate When A List Is Too Much
Reducers consume a sequence and return a single value.
QUERY: $.services.map(@.latency_ms).avg()
OUT: 85.66666666666667
Group-style reducers return summaries that are easy to scan:
QUERY: $.services.count_by(@.owner)
OUT: {"data":1,"platform":2}
6. Build Operator-Friendly Strings
F-strings are useful for logs, labels, report fields, and shell output.
QUERY:
$.services
.filter(@.errors > 0)
.map(f"{@.name}: {@.errors} errors")
OUT:
["api: 2 errors","worker: 9 errors"]
7. Classify Data With Pattern Matching
Pattern matching is a good fit for status payloads, event kinds, and tagged objects.
QUERY:
$.deploys.map(d => match d with {
{status:"fail",service:s} -> f"rollback {s}",
{status:"ok",service:s} -> f"ship {s}",
_ -> "inspect"
})
OUT:
["ship api","rollback worker"]
Arms are checked top-down. Put specific cases before the fallback arm.
8. Search Deeply When The Path Is Not Stable
When you know the condition but not the exact location, use recursive descent.
QUERY: $..find(@.status == "fail")
OUT:
[
{"service":"worker","sha":"b2","status":"fail"}
]
For known schemas, prefer direct paths. For exploratory work over unfamiliar payloads, deep search is often the fastest way to ask the first question.
9. Patch Documents
update returns the full document with the selected changes applied.
QUERY: $.update({"meta.version": @ + 1, "services[*].checked": true})
OUT:
{
"deploys":[
{"service":"api","sha":"a1","status":"ok"},
{"service":"worker","sha":"b2","status":"fail"}
],
"meta":{"env":"prod","version":8},
"services":[
{"checked":true,"enabled":true,"errors":2,"lang":"rust","latency_ms":42,"name":"api","owner":"platform","tags":["edge","json"]},
{"checked":true,"enabled":true,"errors":9,"lang":"go","latency_ms":85,"name":"worker","owner":"data","tags":["queue"]},
{"checked":true,"enabled":false,"errors":0,"lang":"ts","latency_ms":130,"name":"admin","owner":"platform","tags":["internal"]}
]
}
The object keys are paths to update. The expression on the right is evaluated
against the value at that path, so "meta.version": @ + 1 increments the
current version.
10. Row-Local NDJSON
Save this as events.ndjson:
{"ts":"10:00","service":"api","level":"info","ms":38}
{"ts":"10:01","service":"worker","level":"error","ms":220}
{"ts":"10:02","service":"api","level":"error","ms":91}
Run:
jetrocli --ndjson -i events.ndjson -e '$.service + ":" + $.level'
Output:
"api:info"
"worker:error"
"api:error"
Without $.rows(), NDJSON mode evaluates the expression once per line.
11. Whole-Stream NDJSON
Use $.rows() when the expression should see the NDJSON file as one stream.
jetrocli --ndjson -i events.ndjson \
-e '$.rows().filter($.level == "error").map({service: $.service, ms: $.ms})'
Output:
{"service":"worker","ms":220}
{"service":"api","ms":91}
This is the mode for file-level filtering, slicing, grouping, latest-record queries, and compacted-topic inspection.
12. Latest Record Per Key
For Kafka-style records where the payload starts after |:
1|{"id":1,"name":"api old","active":false}
2|{"id":2,"name":"worker","active":true}
1|{"id":1,"name":"api","active":true}
Run:
jetrocli --ndjson -i topic.ndjson --payload-after '|' \
-e '$.rows().reverse().distinct_by($.id).filter($.active).map({id: $.id, name: $.name})'
Output:
{"id":1,"name":"api"}
{"id":2,"name":"worker"}
Read from the end, keep the first row for each id, then filter and project. That is a compacted-topic audit query in one expression.
A Few Power Moves
The tour above keeps to the common path. These examples are worth knowing once you start writing longer queries.
Lambda Forms
The shorthand @ form is usually enough, but named lambdas are useful when an
expression gets dense:
QUERY: $.services.filter(s => s.latency_ms > 80).map(s => s.name)
OUT: ["worker","admin"]
These forms are equivalent where a single current item is in scope:
$.services.filter(@.enabled)
$.services.filter(.enabled)
$.services.filter(lambda s: s.enabled)
Schema Checks
Use has_key for object-key existence, includes for value membership, and
missing for compact schema checks:
QUERY:
$.services.map(s => {
name: s.name,
has_json_tag: s.tags.includes("json"),
missing: s.missing("owner", "tags", "runtime")
})
OUT:
[
{"has_json_tag":true,"missing":["runtime"],"name":"api"},
{"has_json_tag":false,"missing":["runtime"],"name":"worker"},
{"has_json_tag":false,"missing":["runtime"],"name":"admin"}
]
Guards In Pattern Matching
Patterns can bind fields, and guards can refine the match:
QUERY:
$.services.map(s => match s with {
{enabled:false,name:n} -> f"disabled {n}",
{latency_ms:ms,name:n} when ms > 100 -> f"slow {n}",
{name:n} -> f"ok {n}"
})
OUT:
["ok api","ok worker","disabled admin"]
Pipe Value Flow
| passes the value on the left into the right expression as @. It is value
flow, not method dispatch:
QUERY: $.services.count() | "found " + (@ as string) + " services"
OUT: "found 3 services"
Conditional Updates
Filtered wildcards let updates target many items without writing a host loop:
QUERY:
$.services[* if errors > 5].update({
tags: tags.append("hot"),
checked: true
})
The result is still the full document with untouched subtrees preserved.
Demand-Aware Queries
These are ordinary queries:
$.services.map(@.name).last()
$.services.filter(@.enabled).first()
$.services.sort_by(-latency_ms).take(2)
Jetro plans from the demanded result backward. Pure one-to-one maps can be
delayed, first and take can bound input, and tape-backed sources can avoid
materializing values until a stage actually needs them.
Rust Embedding
Use the small facade for one document:
let j = jetro::Jetro::from_bytes(bytes)?;
let out = j.collect("$.services.filter(@.enabled).map(@.name)")?;
Use JetroEngine when you want a long-lived engine with plan and VM reuse:
use jetro::JetroEngine;
use serde_json::json;
let eng = JetroEngine::default();
let doc = json!({"services":[{"latency_ms":42},{"latency_ms":85}]});
let v = eng.collect_value(doc, "$.services.map(@.latency_ms).sum()")?;
assert_eq!(v, json!(127));
What To Read Next
You now have the core mental model: path, chain, project, reduce, patch, and stream.