This guide covers using jjq both as a command-line tool and as an embedded library in Java applications.
Build the CLI from source:
# Requires Java 21+
mvn package -pl jjq-cli -DskipTests
# The executable JAR is at:
java -jar jjq-cli/target/jjq-cli-0.1.1-SNAPSHOT.jar '.name' <<< '{"name":"Alice"}'You can create a shell alias for convenience:
alias jjq='java -jar /path/to/jjq-cli-0.1.1-SNAPSHOT.jar'For instant startup (~3ms vs ~100ms on JVM), build a native binary using GraalVM:
# Requires GraalVM 21+ (or any JDK with native-image installed)
mvn package -pl jjq-core,jjq-cli -Pnative -DskipTests
# The native binary is at jjq-cli/target/jjq
./jjq-cli/target/jjq '.name' <<< '{"name":"Alice"}'The native binary is a self-contained ~17MB executable with no JVM dependency. Copy it anywhere on your PATH:
cp jjq-cli/target/jjq ~/.local/bin/jjqThe CLI module has zero external dependencies (only jjq-core), so native-image compilation requires no reflection configuration or resource bundles.
jjq [OPTIONS] FILTER [FILE...]
jjq reads JSON from files or stdin, applies the filter expression, and writes results to stdout. Like jq, it supports JSONL / NDJSON — multiple whitespace-separated JSON values are each processed independently through the filter.
# From stdin
echo '{"name":"Alice","age":30}' | jjq '.name'
# "Alice"
# From a file
jjq '.users[]' data.json
# From multiple files
jjq '.status' server1.json server2.json server3.json
# JSONL / NDJSON (one JSON value per line)
printf '{"name":"Alice"}\n{"name":"Bob"}\n{"name":"Charlie"}\n' | jjq '.name'
# "Alice"
# "Bob"
# "Charlie"
# Whitespace-separated values also work (not just newlines)
echo '1 2 3' | jjq '. * 10'
# 10
# 20
# 30| Option | Description |
|---|---|
-c, --compact-output |
Compact output (no pretty-printing) |
-r, --raw-output |
Output raw strings without JSON quotes |
-R, --raw-input |
Read each input line as a JSON string |
-s, --slurp |
Collect all inputs into a single array |
-n, --null-input |
Use null as input (ignore stdin/files) |
-e, --exit-status |
Set exit status based on output |
-S, --sort-keys |
Sort object keys alphabetically in output |
-j, --join-output |
Don't print newlines between outputs |
-f, --from-file FILE |
Read filter expression from a file |
-C, --color-output |
Force colored output |
-M, --monochrome-output |
Disable colored output |
--arg NAME VALUE |
Set $NAME to a string value |
--argjson NAME JSON |
Set $NAME to a parsed JSON value |
--tab |
Use tab characters for indentation |
--indent N |
Use N spaces for indentation (default: 2) |
Field access and navigation:
# Simple field
echo '{"name":"Alice","age":30}' | jjq '.name'
# "Alice"
# Nested fields
echo '{"a":{"b":{"c":42}}}' | jjq '.a.b.c'
# 42
# Raw string output (strip quotes)
echo '{"name":"Alice"}' | jjq -r '.name'
# AliceArray operations:
# Iterate elements
echo '[1,2,3]' | jjq '.[]'
# 1
# 2
# 3
# Index and slice
echo '[10,20,30,40,50]' | jjq '.[2]'
# 30
echo '[10,20,30,40,50]' | jjq '.[1:3]'
# [20,30]
# Last two elements
echo '[10,20,30,40,50]' | jjq '.[-2:]'
# [40,50]Filtering and transformation:
# Filter with select
echo '[1,2,3,4,5,6]' | jjq '[.[] | select(. > 3)]'
# [4,5,6]
# Transform each element
echo '[1,2,3,4,5]' | jjq 'map(. * 2)'
# [2,4,6,8,10]
# Filter objects by field value
echo '[{"name":"Alice","age":30},{"name":"Bob","age":25}]' | jjq '[.[] | select(.age >= 30)]'
# [{"name":"Alice","age":30}]Object construction:
# Pick specific fields
echo '{"name":"Alice","email":"a@b.com","age":30,"role":"admin"}' | jjq '{name, email}'
# {"name":"Alice","email":"a@b.com"}
# Rename and reshape
echo '{"first":"Alice","last":"Smith","age":30}' | jjq '{full_name: (.first + " " + .last), age}'
# {"full_name":"Alice Smith","age":30}Aggregation:
# Sum
echo '[1,2,3,4,5]' | jjq 'add'
# 15
echo '[1,2,3,4,5]' | jjq 'reduce .[] as $x (0; . + $x)'
# 15
# Group and count
echo '[{"dept":"eng","name":"A"},{"dept":"sales","name":"B"},{"dept":"eng","name":"C"}]' | \
jjq 'group_by(.dept) | map({dept: .[0].dept, count: length})'
# [{"dept":"eng","count":2},{"dept":"sales","count":1}]Variables:
# String variable
echo '{"items":[1,2,3]}' | jjq --arg key items '.[$key]'
# [1,2,3]
# JSON variable
echo '[1,2,3,4,5]' | jjq --argjson min 3 '[.[] | select(. >= $min)]'
# [3,4,5]Working with JSONL / NDJSON:
# Process a JSONL file (one JSON object per line)
printf '{"name":"Alice","score":90}\n{"name":"Bob","score":75}\n{"name":"Charlie","score":85}\n' > data.jsonl
jjq 'select(.score >= 80) | .name' data.jsonl
# "Alice"
# "Charlie"
# Slurp JSONL into an array for aggregation
jjq -s 'map(.score) | add / length' data.jsonl
# 83.33333333333333
# Multi-line JSON values in a stream (not just single-line)
printf '{\n "a": 1\n}\n{\n "b": 2\n}\n' | jjq -c '.'
# {"a":1}
# {"b":2}
# Mixed types in a stream
printf '"hello"\n42\ntrue\n[1,2]\n' | jjq -c 'type'
# "string"
# "number"
# "boolean"
# "array"Working with files and streams:
# Read filter from file
echo '[.[] | select(.active)] | length' > filter.jq
jjq -f filter.jq users.json
# Slurp multiple inputs into one array
echo '1' > a.json
echo '2' > b.json
jjq -s 'add' a.json b.json
# 3
# Compact output (for piping)
echo '{"a":1,"b":2}' | jjq -c '.a'
# 1
# Null input (generate data)
jjq -n '{now: now | todate}'Add jjq to your Maven project:
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-core</artifactId>
<version>0.1.1-SNAPSHOT</version>
</dependency>jjq-core has zero external dependencies. For fastjson2 integration, add:
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-fastjson2</artifactId>
<version>0.1.1-SNAPSHOT</version>
</dependency>jjq follows a compile-once, apply-many pattern. A JqProgram is compiled from a jq expression string and can then be applied to any number of inputs. Programs are immutable and thread-safe.
import io.hyperfoil.tools.jjq.JqProgram;
import io.hyperfoil.tools.jjq.value.JqValue;
import io.hyperfoil.tools.jjq.value.JqValues;
// 1. Compile the program (do this once)
JqProgram program = JqProgram.compile(".users[] | {name, email}");
// 2. Parse or build input
JqValue input = JqValues.parse("""
{"users": [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"}
]}
""");
// 3. Apply and get results
JqValue first = program.apply(input); // first result only
List<JqValue> all = program.applyAll(input); // all resultsJqProgram provides several execution methods. The primary API uses the bytecode VM, which is the fastest execution engine (up to 16x faster than alternatives). Always prefer the VM methods.
| Method | Engine | Returns | Best for |
|---|---|---|---|
apply(input) |
VM | First JqValue |
Single-output filters (field access, arithmetic, reduce, object construction). Zero-allocation for simple programs. |
applyAll(input) |
VM | List<JqValue> |
Multi-output filters (.[], generators, comma expressions) |
apply(input, output) |
VM | void (streams to Consumer) |
Processing results without collecting into a list |
Use apply() by default. It returns a single JqValue with zero allocation overhead for programs that produce exactly one output. This covers the vast majority of use cases: field access, arithmetic, object construction, reduce, array construction ([...]), and any filter wrapped in [...].
// RECOMMENDED: apply() for single-output programs
JqProgram getName = JqProgram.compile(".name");
JqValue name = getName.apply(input); // zero-allocation, returns JqValue directly
// Use applyAll() only when multiple outputs are expected
JqProgram iterate = JqProgram.compile(".users[]");
List<JqValue> users = iterate.applyAll(input); // returns all outputs
// Stream results without collecting
JqProgram stream = JqProgram.compile(".events[]");
stream.apply(input, event -> processEvent(event));How to know which method to use:
- If your filter is wrapped in
[...](array construction), it always produces one output — useapply(). - Filters like
.field,.a.b.c,. + 1,reduce,{key: .val}produce one output — useapply(). - Filters like
.[],.a, .b,range(n)produce multiple outputs — useapplyAll(). - When in doubt, use
applyAll()— it works for any number of outputs.
In enterprise applications, you often have multiple JSON objects in memory — from a message queue, database results, or API responses — that you want to process through the same filter. jjq supports this natively with multi-input methods that reuse a single VM instance for efficiency.
Parsing JSONL strings:
// Parse a JSONL / NDJSON string into multiple values
List<JqValue> inputs = JqValues.parseAll("""
{"name":"Alice","age":30}
{"name":"Bob","age":25}
{"name":"Charlie","age":35}
""");
// Returns 3 separate JqValue objectsProcessing multiple in-memory values:
JqProgram program = JqProgram.compile("select(.age >= 30) | .name");
// Option 1: Collect all results into a list
List<JqValue> inputs = List.of(user1, user2, user3);
List<JqValue> results = program.applyAll(inputs);
// Option 2: Stream results lazily
program.stream(inputs).forEach(name ->
System.out.println(name.stringValue())
);
// Option 3: Callback for each result
program.applyAll(inputs, result ->
sendToDownstream(result)
);With variables:
Environment env = new Environment();
env.setVariable("minAge", JqNumber.of(30));
JqProgram program = JqProgram.compile("select(.age >= $minAge)");
List<JqValue> results = program.applyAll(inputs, env);Why use applyAll(Iterable) instead of looping with apply()?
The multi-input methods create one VirtualMachine and reuse it across all inputs. Calling apply() or applyAll(singleInput) in a loop creates a new VM each time, re-allocating stacks and re-analyzing the program. For high-throughput processing (thousands of records), the multi-input API is measurably faster.
// GOOD: single VM reused across all inputs
List<JqValue> results = program.applyAll(inputs);
// ALSO FINE but slower: new VM per input
List<JqValue> results = new ArrayList<>();
for (JqValue input : inputs) {
results.addAll(program.applyAll(input)); // new VM each time
}jjq uses its own value types that map directly to JSON:
| JSON Type | jjq Type | Create | Extract |
|---|---|---|---|
null |
JqNull |
JqNull.NULL |
value.isNull() |
true/false |
JqBoolean |
JqBoolean.of(true) |
value.booleanValue() |
| numbers | JqNumber |
JqNumber.of(42), JqNumber.of(3.14) |
value.longValue(), value.doubleValue() |
| strings | JqString |
JqString.of("hello") |
value.stringValue() |
| arrays | JqArray |
JqArray.of(elem1, elem2) |
value.arrayValue() (returns List<JqValue>) |
| objects | JqObject |
JqObject.of(map) |
value.objectValue() (returns Map<String, JqValue>) |
Parsing JSON strings:
JqValue value = JqValues.parse("{\"name\":\"Alice\",\"age\":30}");Building values programmatically:
// Build without parsing JSON — useful when data comes from Java objects
JqValue user = JqObject.of(Map.of(
"name", JqString.of("Alice"),
"age", JqNumber.of(30),
"roles", JqArray.of(JqString.of("admin"), JqString.of("user"))
));Extracting values from results:
JqProgram program = JqProgram.compile("{name, age}");
JqValue result = program.apply(input);
// Type-check before extracting
if (result.isObject()) {
Map<String, JqValue> obj = result.objectValue();
String name = obj.get("name").stringValue();
long age = obj.get("age").longValue();
}
// Or use JqObject.get() which returns JqNull for missing keys
JqValue nameVal = ((JqObject) result).get("name"); // never null, returns JqNull.NULL if missingType checking:
JqValue val = program.apply(input);
switch (val.type()) {
case NULL -> handleNull();
case BOOLEAN -> handleBoolean(val.booleanValue());
case NUMBER -> handleNumber(val.longValue());
case STRING -> handleString(val.stringValue());
case ARRAY -> handleArray(val.arrayValue());
case OBJECT -> handleObject(val.objectValue());
}
// Or use pattern matching
if (val instanceof JqString s) {
System.out.println(s.stringValue());
} else if (val instanceof JqNumber n) {
System.out.println(n.longValue());
}Value properties:
- All
JqValueinstances are immutable — safe to cache, store, and share. JqNumberuses alongfast-path internally, falling back toBigDecimalfor decimals. NaN and Infinity are supported.JqObjectpreserves insertion order (backed byLinkedHashMap).JqArrayis backed by an unmodifiableList<JqValue>.
Use Environment to pass variables into jq expressions. This is the safe way to inject values — no string concatenation or injection risk.
import io.hyperfoil.tools.jjq.evaluator.Environment;
JqProgram query = JqProgram.compile("[.[] | select(.dept == $department)]");
Environment env = new Environment();
env.setVariable("department", JqString.of("engineering"));
JqValue result = query.apply(input, env);Variables can be any JqValue type:
env.setVariable("name", JqString.of("Alice")); // string
env.setVariable("threshold", JqNumber.of(100)); // number
env.setVariable("tags", JqArray.of( // array
JqString.of("prod"), JqString.of("us-east")
));
env.setVariable("active", JqBoolean.TRUE); // booleanImportant: Environment objects are not thread-safe. Create a new Environment per request or per thread. The JqProgram itself is thread-safe — only the Environment needs to be separate.
jjq uses unchecked exceptions. All exceptions extend RuntimeException.
import io.hyperfoil.tools.jjq.evaluator.JqException;
import io.hyperfoil.tools.jjq.parser.ParseException;
import io.hyperfoil.tools.jjq.value.JqTypeError;
// Compile-time errors (bad syntax)
try {
JqProgram.compile(".foo ||| .bar");
} catch (ParseException e) {
// Invalid filter syntax
System.err.println("Parse error: " + e.getMessage());
}
// Runtime errors (type mismatches, missing fields on wrong types)
try {
JqValue result = program.apply(input);
} catch (JqTypeError e) {
// e.g., trying to index a number with a string
System.err.println("Type error: " + e.getMessage());
} catch (JqException e) {
// General runtime error
System.err.println("jq error: " + e.getMessage());
}For user-facing applications, wrap compilation and execution separately:
public JqProgram compileUserFilter(String filter) {
try {
return JqProgram.compile(filter);
} catch (ParseException e) {
throw new BadRequestException("Invalid filter: " + e.getMessage());
}
}
public JqValue executeFilter(JqProgram program, JqValue input) {
try {
return program.apply(input);
} catch (JqException e) {
throw new ProcessingException("Filter execution failed: " + e.getMessage());
}
}If your application already uses fastjson2, the FastjsonEngine provides convenient integration:
import io.hyperfoil.tools.jjq.fastjson2.FastjsonEngine;
FastjsonEngine engine = new FastjsonEngine();
// One-liner: compile + parse + apply
List<JqValue> results = engine.apply(".name", "{\"name\":\"Alice\"}");
// Get results as JSON strings
List<String> jsonStrings = engine.applyToStrings("[.[] | . * 2]", "[1,2,3]");Byte buffer processing (no intermediate String creation):
byte[] jsonBytes = readFromNetwork();
JqProgram program = engine.compile("{loc: .location, temp: .temperature}");
byte[] resultBytes = engine.applyToBytes(program, jsonBytes);
writeToNetwork(resultBytes);JSON Lines / NDJSON stream processing:
JqProgram filter = engine.compile("select(.level == \"ERROR\") | {msg, ts}");
InputStream logStream = openLogFile();
Stream<JqValue> errors = engine.applyToJsonStream(filter, logStream);
errors.forEach(err -> alertOnError(err));Lazy conversion (for large documents where you only access a subset):
// Only converts nested values when actually accessed by the filter
JqValue lazy = FastjsonEngine.fromJsonLazy(largeJsonString);
JqValue id = JqProgram.compile(".metadata.id").apply(lazy);
// The rest of the document (e.g., a 10MB payload array) was never convertedfastjson2 interop:
// Convert fastjson2 objects to JqValue
JSONObject obj = JSONObject.parseObject(json);
JqValue input = FastjsonEngine.fromFastjson(obj);
// Convert results back to fastjson2
JqValue result = program.apply(input);
JSONObject output = (JSONObject) FastjsonEngine.toFastjson(result);JqProgram is the central thread-safety primitive in jjq. Understanding what is safe to share and what must be per-thread is critical for server applications.
Thread-safe (share freely):
JqProgram— immutable after compilation; share across all threads- All
JqValueinstances — immutable by design BuiltinRegistry.getDefault()— singleton, read-only after initializationFastjsonEngine— stateless, safe to share
Not thread-safe (create per request/thread):
Environment— mutable variable store; create a new instance per request
Pattern for concurrent use:
public class JqService {
// Compile once at startup — thread-safe, share across all requests
private final JqProgram extractUser = JqProgram.compile("{name, email, role}");
private final JqProgram filterActive = JqProgram.compile("[.[] | select(.active)]");
private final JqProgram summarize = JqProgram.compile(
"{total: length, by_role: (group_by(.role) | map({role: .[0].role, count: length}))}"
);
public JqValue getUser(JqValue data) {
return extractUser.apply(data); // safe to call concurrently
}
public JqValue queryByDept(JqValue data, String dept) {
JqProgram query = JqProgram.compile("[.[] | select(.dept == $dept)]");
Environment env = new Environment(); // per-request
env.setVariable("dept", JqString.of(dept));
return query.apply(data, env);
}
}-
Always use
apply()overapplyAll()when you expect a single result. Theapply()method uses a zero-allocation code path in the VM for single-output programs like field access, arithmetic, reduce, and array/object construction. This is measurably faster. -
Compile programs once and reuse. Compilation parses the expression and generates bytecode. Reusing the compiled
JqProgramavoids this cost on every request.// GOOD: compile once private static final JqProgram TRANSFORM = JqProgram.compile("{name, age}"); public JqValue transform(JqValue input) { return TRANSFORM.apply(input); } // BAD: re-compiles on every call public JqValue transformSlow(JqValue input) { return JqProgram.compile("{name, age}").apply(input); }
-
Build
JqValuedirectly instead of parsing JSON strings when data originates from Java objects. Parsing JSON is fast but building values directly avoids the round-trip.// GOOD: build directly from Java data JqValue input = JqObject.of(Map.of( "name", JqString.of(user.getName()), "score", JqNumber.of(user.getScore()) )); // ALSO FINE: parse JSON (convenient for external data) JqValue input = JqValues.parse(jsonString);
-
Use
FastjsonEngine.fromJsonLazy()for large documents where only a subset of fields are accessed. This defers conversion of nested arrays and objects until they are actually traversed. -
Use byte buffer APIs (
FastjsonEngine.applyToBytes()) for network services to avoidStringallocation for large payloads. -
Wrap multiple outputs in
[...]when possible to get a single array result and useapply()instead ofapplyAll().// Prefer this (single output, use apply()) JqProgram prog = JqProgram.compile("[.users[] | .name]"); JqValue names = prog.apply(input); // returns ["Alice","Bob"] // Over this (multiple outputs, requires applyAll()) JqProgram prog = JqProgram.compile(".users[] | .name"); List<JqValue> names = prog.applyAll(input); // returns ["Alice", "Bob"] as separate items
Use jjq to build flexible API response transformations without hardcoding field mappings:
@Path("/api/data")
public class DataResource {
// Pre-compiled transforms for known use cases
private static final JqProgram SUMMARY = JqProgram.compile(
"{total: (.items | length), revenue: ([.items[].price] | add)}"
);
@GET
@Path("/summary")
public Response getSummary() {
JqValue data = loadData();
JqValue summary = SUMMARY.apply(data);
return Response.ok(summary.toJsonString()).build();
}
// Let clients specify their own projection
@GET
@Path("/query")
public Response query(@QueryParam("filter") String filter) {
// Validate and compile the user's filter
JqProgram program;
try {
program = JqProgram.compile(filter);
} catch (ParseException e) {
return Response.status(400)
.entity("{\"error\":\"Invalid filter: " + e.getMessage() + "\"}")
.build();
}
try {
JqValue data = loadData();
JqValue result = program.apply(data);
return Response.ok(result.toJsonString()).build();
} catch (JqException e) {
return Response.status(422)
.entity("{\"error\":\"Filter execution failed: " + e.getMessage() + "\"}")
.build();
}
}
}Apply jq filters to messages from Kafka, RabbitMQ, or other message queues:
public class MessageProcessor {
private final JqProgram extractPayload = JqProgram.compile(".payload");
private final JqProgram routingFilter = JqProgram.compile(
"select(.headers.type == $msgType) | .payload"
);
public void processMessage(byte[] messageBytes) {
// Use byte buffer processing for efficiency
FastjsonEngine engine = new FastjsonEngine();
JqValue message = FastjsonEngine.fromJson(new String(messageBytes));
JqValue payload = extractPayload.apply(message);
handlePayload(payload);
}
public void processFiltered(JqValue message, String messageType) {
Environment env = new Environment();
env.setVariable("msgType", JqString.of(messageType));
List<JqValue> results = routingFilter.applyAll(message, env);
// Empty list means the message didn't match the filter (select returned empty)
for (JqValue payload : results) {
handlePayload(payload);
}
}
}Use jq expressions to navigate complex configuration structures:
public class ConfigService {
private final JqValue config;
public ConfigService(String configJson) {
this.config = JqValues.parse(configJson);
}
// Type-safe config extraction with jq
public String getString(String jqPath) {
JqValue val = JqProgram.compile(jqPath).apply(config);
return val.isString() ? val.stringValue() : null;
}
public long getLong(String jqPath, long defaultValue) {
JqValue val = JqProgram.compile(jqPath).apply(config);
return val.isNumber() ? val.longValue() : defaultValue;
}
public boolean isEnabled(String feature) {
JqProgram check = JqProgram.compile(
".features." + feature + ".enabled // false"
);
return check.apply(config).booleanValue();
}
public List<String> getEnabledFeatures() {
JqProgram query = JqProgram.compile(
"[.features | to_entries[] | select(.value.enabled) | .key]"
);
JqValue result = query.apply(config);
return result.arrayValue().stream()
.map(JqValue::stringValue)
.toList();
}
}Let users save and execute their own jq queries against application data. This is useful for dashboards, reporting tools, and data exploration features.
public class QueryEngine {
// Cache compiled programs to avoid re-compilation
private final ConcurrentHashMap<String, JqProgram> programCache = new ConcurrentHashMap<>();
public JqProgram getOrCompile(String expression) {
return programCache.computeIfAbsent(expression, expr -> {
try {
return JqProgram.compile(expr);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid query: " + e.getMessage(), e);
}
});
}
public String executeQuery(String expression, String inputJson,
Map<String, String> variables) {
JqProgram program = getOrCompile(expression);
JqValue input = JqValues.parse(inputJson);
Environment env = new Environment();
for (var entry : variables.entrySet()) {
env.setVariable(entry.getKey(), JqString.of(entry.getValue()));
}
JqValue result = program.apply(input, env);
return result.toJsonString();
}
}Use jq to compute metrics from structured monitoring data:
public class MetricsAggregator {
private static final JqProgram ERROR_RATE = JqProgram.compile("""
{
total: length,
errors: (map(select(.status >= 400)) | length),
error_pct: ((map(select(.status >= 400)) | length) * 100.0 / length)
}
""");
private static final JqProgram LATENCY_STATS = JqProgram.compile("""
{
p50: (sort | .[length / 2 | floor]),
p95: (sort | .[length * 0.95 | floor]),
p99: (sort | .[length * 0.99 | floor]),
avg: (add / length),
max: max
}
""");
private static final JqProgram PER_ENDPOINT = JqProgram.compile("""
group_by(.endpoint) | map({
endpoint: .[0].endpoint,
count: length,
avg_latency: (map(.latency_ms) | add / length | floor),
error_count: (map(select(.status >= 400)) | length)
})
""");
public JqValue computeErrorRate(JqValue requestLogs) {
return ERROR_RATE.apply(requestLogs);
}
public JqValue computeLatencyStats(JqValue latencyValues) {
return LATENCY_STATS.apply(latencyValues);
}
public JqValue computePerEndpoint(JqValue requestLogs) {
return PER_ENDPOINT.apply(requestLogs);
}
}Chain jq transformations as a lightweight data pipeline:
public class DataPipeline {
private final List<JqProgram> stages;
public DataPipeline(String... expressions) {
this.stages = Arrays.stream(expressions)
.map(JqProgram::compile)
.toList();
}
public JqValue process(JqValue input) {
JqValue current = input;
for (JqProgram stage : stages) {
current = stage.apply(current);
}
return current;
}
// Example usage
public static void main(String[] args) {
DataPipeline pipeline = new DataPipeline(
// Stage 1: Filter to production data
"{results: [.results[] | select(.tags | contains([\"prod\"]))]}",
// Stage 2: Extract metrics
".results | map({host: .host, cpu: .cpu})",
// Stage 3: Compute summary
"{count: length, avg_cpu: (map(.cpu) | add / length), max_cpu: (map(.cpu) | max)}"
);
JqValue input = JqValues.parse(loadMonitoringData());
JqValue summary = pipeline.process(input);
}
}Process large numbers of records efficiently using the multi-input API, which reuses a single VM across all inputs:
public class BatchProcessor {
private final JqProgram transform;
public BatchProcessor(String expression) {
this.transform = JqProgram.compile(expression);
}
// Process all records through a single VM (most efficient)
public List<JqValue> processAll(List<JqValue> records) {
return transform.applyAll(records);
}
// Stream results without collecting
public void processAll(List<JqValue> records, Consumer<JqValue> handler) {
transform.applyAll(records, handler);
}
// Process records in parallel (JqProgram is thread-safe)
public List<JqValue> processParallel(List<JqValue> records) {
return records.parallelStream()
.map(transform::apply)
.toList();
}
// Parse and process a JSONL string
public List<JqValue> processJsonl(String jsonlContent) {
List<JqValue> inputs = JqValues.parseAll(jsonlContent);
return transform.applyAll(inputs);
}
// Stream processing with fastjson2
public void processStream(InputStream ndjsonInput, Consumer<JqValue> handler) {
FastjsonEngine engine = new FastjsonEngine();
engine.applyToJsonStream(transform, ndjsonInput)
.forEach(handler);
}
}This is a quick reference for jq syntax supported by jjq. For the complete jq manual, see jqlang.github.io/jq/manual.
| Syntax | Description | Example |
|---|---|---|
. |
Identity | . => input unchanged |
.foo |
Field access | .name => "Alice" |
.foo.bar |
Nested field | .a.b => value at a.b |
.foo? |
Optional field (no error if missing) | .missing? => nothing |
.[n] |
Array index | .[0] => first element |
.[m:n] |
Array slice | .[1:3] => elements 1,2 |
.[] |
Iterate all elements | .[] => each element |
.[]? |
Optional iterate | .[]? => each element or nothing |
| Syntax | Description |
|---|---|
| |
Pipe (chain filters) |
, |
Multiple outputs |
+, -, *, /, % |
Arithmetic |
==, !=, <, <=, >, >= |
Comparison |
and, or, not |
Logical |
// |
Alternative (default value) |
| Syntax | Description |
|---|---|
[expr] |
Array construction |
{key: expr} |
Object construction |
{name} |
Shorthand for {name: .name} |
{(expr): expr} |
Computed key |
"Hello \(expr)" |
String interpolation |
| Syntax | Description |
|---|---|
if cond then a elif cond then b else c end |
Conditional |
try expr |
Suppress errors |
try expr catch handler |
Handle errors |
label $name | expr |
Label for break |
| Syntax | Description |
|---|---|
expr as $var | body |
Variable binding |
expr as [$a, $b] | body |
Destructuring array bind |
expr as {key: $v} | body |
Destructuring object bind |
reduce .[] as $x (init; update) |
Reduce/fold |
foreach .[] as $x (init; update) |
Stateful iteration |
foreach .[] as $x (init; update; extract) |
With extraction |
def name(params): body; |
Function definition |
| Syntax | Description |
|---|---|
.foo = expr |
Set field |
.foo |= expr |
Update field |
.foo += expr |
Add-assign |
.foo -= expr |
Subtract-assign |
.foo *= expr |
Multiply-assign |
.foo //= expr |
Alternative-assign |
Type and conversion: type, length, keys, values, has(key), in(obj), contains(val), tostring, tonumber, ascii_upcase, ascii_downcase
Arrays: map(f), select(f), sort, sort_by(f), group_by(f), unique, unique_by(f), reverse, flatten, add, any, all, first, last, nth(n), range(n), min, max, min_by(f), max_by(f), indices(val), transpose, combinations
Strings: split(sep), join(sep), test(regex), match(regex), capture(regex), sub(re; rep), gsub(re; rep), ltrimstr(s), rtrimstr(s), startswith(s), endswith(s), ascii_upcase, ascii_downcase
Objects: to_entries, from_entries, with_entries(f), keys, values, has(key)
Paths: path(expr), paths, paths(filter), getpath(p), setpath(p; v), delpaths(ps), del(expr)
Math: floor, ceil, round, sqrt, log, log2, log10, exp, pow(x;y), fabs, nan, infinite, isinfinite, isnan, isnormal
I/O formats: @base64, @base64d, @uri, @csv, @tsv, @html, @json, @sh, @text, tojson, fromjson
Date/time: now, todate, fromdate, gmtime, mktime, strftime(fmt), strptime(fmt)
Iteration: recurse, recurse(f), walk(f), limit(n; f), first(f), last(f), until(cond; update), while(cond; update), repeat(f)
jjq passes 491 of 508 upstream jq tests (96.7%). The remaining differences are documented below so you can determine whether they affect your use case.
jjq does not implement jq's module system. The import, include, and modulemeta keywords will produce a parse error. This means you cannot split jq code across multiple files using jq's built-in module mechanism.
Workaround: Define functions inline within your filter, or compose programs at the Java API level by chaining multiple JqProgram instances in a pipeline.
jq uses arbitrary-precision integers internally and clamps values to IEEE 754 double precision only on output. jjq uses long with BigDecimal fallback, which can produce slightly different results for integers beyond the safe 64-bit floating-point range (greater than 2^53).
# jq: 13911860366432393 - 10 => 13911860366432382
# jjq: 13911860366432393 - 10 => 13911860366432383
Normal-range integer and floating-point arithmetic works correctly. This only affects edge cases with very large integers that exceed the precision of IEEE 754 doubles.
- fromjson parse errors: Column numbers in JSON parse error messages may differ from jq (e.g., column 2 vs column 5) due to differences in the underlying JSON parser.
// Compilation
static JqProgram compile(String expression)
static JqProgram compile(String expression, BuiltinRegistry builtins)
// Primary API (bytecode VM — recommended)
JqValue apply(JqValue input) // first result, zero-alloc
JqValue apply(JqValue input, Environment env) // with variables
List<JqValue> applyAll(JqValue input) // all results
List<JqValue> applyAll(JqValue input, Environment env)
void apply(JqValue input, Consumer<JqValue> output) // streaming
// Multi-input API (JSONL-style, reuses single VM)
List<JqValue> applyAll(Iterable<JqValue> inputs) // all inputs, all results
List<JqValue> applyAll(Iterable<JqValue> inputs, Environment env)
void applyAll(Iterable<JqValue> inputs, Consumer<JqValue> output)
Stream<JqValue> stream(Iterable<JqValue> inputs) // stream of results
// Utilities
String expression() // original filter string
Bytecode getBytecode() // compiled bytecodestatic JqValue parse(String json) // parse a single JSON value
static List<JqValue> parseAll(String json) // parse JSONL / whitespace-separated JSON values// Type
Type type() // NULL, BOOLEAN, NUMBER, STRING, ARRAY, OBJECT
boolean isNull(), isBoolean(), isNumber(), isString(), isArray(), isObject()
boolean isTruthy() // false and null are falsy
// Value extraction
boolean booleanValue()
long longValue()
double doubleValue()
BigDecimal decimalValue()
String stringValue()
List<JqValue> arrayValue()
Map<String, JqValue> objectValue()
// Operators
JqValue add(JqValue other) // +
JqValue subtract(JqValue other) // -
JqValue multiply(JqValue other) // *
JqValue divide(JqValue other) // /
JqValue modulo(JqValue other) // %
JqValue negate() // unary -
// Output
String toJsonString()
int length()void setVariable(String name, JqValue value)
JqValue getVariable(String name)
boolean hasVariable(String name)
Environment child()// Compilation
JqProgram compile(String expression)
// Apply
List<JqValue> apply(String expression, String json)
List<String> applyToStrings(String expression, String json)
// Byte buffers
byte[] applyToBytes(String expression, byte[] jsonBytes)
byte[] applyToBytes(JqProgram program, byte[] buffer, int offset, int length)
// Streaming
Stream<JqValue> applyToJsonStream(JqProgram program, InputStream input)
Stream<JqValue> applyToJsonStream(JqProgram program, byte[] buffer, int offset, int length)
// Conversion
static JqValue fromJson(String json)
static JqValue fromJsonLazy(String json)
static JqValue fromFastjson(Object obj)
static JqValue fromFastjsonLazy(Object obj)
static Object toFastjson(JqValue value)