High-performance pure Java jq implementation backed by fastjson2.
jjq provides a complete jq filter engine with zero native dependencies, making it portable across all JVM platforms. It uses a bytecode-compiled VM for optimal performance.
- Full jq syntax — pipes, field access, iteration, array/object construction, string interpolation, reduce, foreach, try-catch, label-break, destructuring bind, function definitions, and more
- 179 builtin functions — comprehensive coverage of jq's standard library including math, string, array, object, path, date/time, and format operations
- Bytecode VM — up to 18x faster than jackson-jq with constant folding, peephole optimizations, and fast-path shape detection
- fastjson2 integration — lazy zero-copy conversion, byte buffer processing, and JSON stream support
- Thread-safe — compiled programs are immutable and can be shared across threads
- Java 21+ — leverages sealed classes, records, and pattern matching
| Module | Description |
|---|---|
jjq-core |
Lexer, parser, AST, evaluator, bytecode VM, builtins (zero external dependencies) |
jjq-jackson |
Jackson databind adapter — JsonNode ↔ JqValue conversion |
jjq-fastjson2 |
fastjson2 adapter with lazy conversion and streaming APIs |
jjq-cli |
Command-line interface (zero dependencies, GraalVM native-image ready) |
jjq-test-suite |
466 conformance tests + 508 upstream jq tests (96.7% passing) |
jjq-benchmark |
JMH benchmarks comparing jjq VM and jackson-jq |
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<!-- For Jackson integration (Quarkus, Spring, etc.) -->
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-jackson</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<!-- For fastjson2 integration -->
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-fastjson2</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>Core API (zero dependencies):
import io.hyperfoil.tools.jjq.JqProgram;
import io.hyperfoil.tools.jjq.value.JqValues;
import io.hyperfoil.tools.jjq.value.JqValue;
// Compile once, apply many times (thread-safe)
JqProgram program = JqProgram.compile(".users[] | {name, email}");
JqValue input = JqValues.parse("""
{"users": [
{"name": "Alice", "email": "alice@example.com", "age": 30},
{"name": "Bob", "email": "bob@example.com", "age": 25}
]}
""");
List<JqValue> results = program.apply(input);
results.forEach(r -> System.out.println(r.toJsonString()));
// {"name":"Alice","email":"alice@example.com"}
// {"name":"Bob","email":"bob@example.com"}Jackson integration (for Quarkus / REST APIs):
import io.hyperfoil.tools.jjq.jackson.JacksonJqEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
JacksonJqEngine engine = new JacksonJqEngine(mapper);
// Pre-compile filter (thread-safe, reuse across requests)
JqProgram program = engine.compile(".users[] | {name, email}");
// Apply to Jackson JsonNode — input and output are both JsonNode
JsonNode input = mapper.readTree(requestBody);
List<JsonNode> results = engine.apply(program, input);
// Or get the first result directly
JsonNode first = engine.applyFirst(program, input);With variables:
import io.hyperfoil.tools.jjq.evaluator.Environment;
import io.hyperfoil.tools.jjq.value.JqString;
JqProgram program = JqProgram.compile(".[] | select(.name == $target)");
Environment env = new Environment();
env.setVariable("target", JqString.of("Alice"));
List<JqValue> results = program.apply(input, env);Processing multiple inputs (JSONL-style):
// 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}
""");
// Process all inputs through one filter — reuses a single VM for efficiency
JqProgram program = JqProgram.compile(".name");
List<JqValue> names = program.applyAll(inputs);
// "Alice", "Bob", "Charlie"
// Or stream results
program.stream(inputs).forEach(name -> System.out.println(name.toJsonString()));FastjsonEngine (high-level API with fastjson2):
import io.hyperfoil.tools.jjq.fastjson2.FastjsonEngine;
FastjsonEngine engine = new FastjsonEngine();
// String in, results out
List<JqValue> results = engine.apply(".name", "{\"name\": \"Alice\"}");
// Compile and reuse
JqProgram program = engine.compile("[.[] | . * 2]");
// Byte buffer mode
byte[] output = engine.applyToBytes(program, jsonBytes);
// JSON stream processing (multiple JSON values)
Stream<JqValue> stream = engine.applyToJsonStream(program, inputStream);
// Lazy parsing (converts nested values on demand)
JqValue lazy = FastjsonEngine.fromJsonLazy(largeJsonString);jjq [OPTIONS] FILTER [FILE...]
| Option | Description |
|---|---|
-c, --compact-output |
Compact output (no pretty-printing) |
-r, --raw-output |
Output raw strings (no JSON quotes) |
-R, --raw-input |
Read each input line as a string |
-s, --slurp |
Read all inputs into an array |
-n, --null-input |
Use null as input |
-e, --exit-status |
Set exit status based on output |
-S, --sort-keys |
Sort object keys in output |
-j, --join-output |
Don't print newlines between outputs |
-f, --from-file FILE |
Read filter from file |
-C, --color-output |
Force colored output |
-M, --monochrome-output |
Disable colored output |
--arg NAME VALUE |
Set $NAME to string VALUE |
--argjson NAME JSON |
Set $NAME to parsed JSON value |
--tab |
Use tab for indentation |
--indent N |
Use N spaces for indentation (default: 2) |
# Field access
echo '{"name":"Alice","age":30}' | jjq '.name'
# "Alice"
# Filter and transform
echo '[1,2,3,4,5]' | jjq '[.[] | select(. > 2) | . * 10]'
# [30,40,50]
# Object construction
echo '{"first":"Alice","last":"Smith","age":30}' | jjq '{full: (.first + " " + .last), age}'
# {"full":"Alice Smith","age":30}
# Reduce
echo '[1,2,3,4,5]' | jjq 'reduce .[] as $x (0; . + $x)'
# 15
# With variables
echo '{"items":[1,2,3]}' | jjq --arg name items '.[$name]'
# [1,2,3]
# Read filter from file
jjq -f filter.jq data.json
# Process JSONL / NDJSON (one JSON value per line)
printf '{"name":"Alice"}\n{"name":"Bob"}\n' | jjq '.name'
# "Alice"
# "Bob"
# Slurp JSONL into array
printf '1\n2\n3\n' | jjq -s 'add'
# 6jq expression string
|
[Lexer] Hand-written, character-by-character
|
Token stream
|
[Parser] Pratt parser (top-down operator precedence)
|
AST (JqExpr) ~35 sealed record types with source locations
|
+-----------+-----------+
| |
[Compiler]
AST -> Bytecode
|
[VM]
Stack-based with
FORK/BACKTRACK
for generators
|
Output
The VM compiles jq expressions to 72 opcodes and executes them on a stack machine. Key design features:
- FORK/BACKTRACK for jq's generator semantics (multiple outputs per expression)
- 21 inlined builtin opcodes (length, type, keys, sort, etc.) avoiding interpreter overhead
- DOT_FIELD2 compound instruction for
.a.bchained field access - Fused iteration opcodes (COLLECT_ITERATE, REDUCE_ITERATE) that bypass backtracking for common patterns
- Parallel array dispatch — bytecode stored as parallel
int[]arrays for zero-overhead opcode access - Program shape detection — identity, field access, and pipe-arith patterns bypass the VM entirely
- Constant folding evaluates literal expressions (
2 + 3->5) at compile time - Peephole optimization removes no-op instruction sequences
- Pre-allocated growable stacks for minimal allocation during execution
| Type | Implementation |
|---|---|
JqNull |
Singleton |
JqBoolean |
boolean with TRUE/FALSE constants |
JqNumber |
long fast-path, BigDecimal fallback, NaN/Infinity support |
JqString |
String |
JqArray |
List<JqValue> |
JqObject |
LinkedHashMap<String, JqValue> (preserves insertion order) |
# Requires Java 21+
mvn clean install
# Run tests only
mvn test
# Build CLI
mvn package -pl jjq-cli
# Build native binary (requires GraalVM 21+)
mvn package -pl jjq-core,jjq-cli -Pnative -DskipTests
# The native binary is at:
./jjq-cli/target/jjq '.name' <<< '{"name":"Alice"}'
# Run benchmarks
mvn package -pl jjq-benchmark -DskipTests
java -jar jjq-benchmark/target/jjq-benchmark-0.1.0-SNAPSHOT.jarjjq VM vs jackson-jq throughput (ops/μs, higher is better):
| Benchmark | jackson-jq | jjq VM | Ratio |
|---|---|---|---|
identity (.) |
234.5 | 479.0 | 2.0x |
fieldAccess (.foo) |
69.5 | 135.3 | 1.9x |
pipeArith (.a | . + 1) |
32.6 | 95.1 | 2.9x |
iterateMap ([.[] | . * 2], 10 elem) |
5.4 | 18.7 | 3.5x |
| iterateMap (100 elem) | 0.55 | 3.41 | 6.2x |
| complexFilter | 0.56 | 4.39 | 7.9x |
reduce (reduce .[] as $x (0; . + $x)) |
2.64 | 41.6 | 15.8x |
Measured with JMH on Temurin JDK 21.0.6, 2 forks × 5 iterations.
- Identity (
.), field access (.foo,.a.b.c), indexing (.[0],.[2:5]) - Pipes (
|), comma (,), parentheses - Array/object construction (
[...],{...}, computed keys) - String interpolation (
"Hello \(.name)") - Arithmetic (
+,-,*,/,%), comparison, logical operators - Recursive object merge (
* operator) - Alternative operator (
//) - Optional operator (
.foo?,.[]?) if-then-elif-else-endtry-catch- Variable binding (
. as $x | ...) - Destructuring bind (
. as [$a, $b] | ...,. as {name: $n} | ...) reduce,foreach(with destructuring pattern support)- Function definitions (
def f(x): ...;) with proper closure scoping - Function arguments as path expressions (
def inc(x): x |= .+1;) label-break- Assignment operators (
|=,+=,-=,*=,/=,%=,//=) - Path expressions (
path(),getpath,setpath,delpaths,del) - Complex path expressions (
path(.foo[0,1]),path(..),del(.[] | select(...))) - Recursive descent (
..) - Format strings (
@base64,@uri,@csv,@tsv,@html,@json) - All standard builtins (179 functions)
jjq passes 491 of 508 upstream jq tests (96.7%). The remaining differences fall into these categories:
jjq does not implement jq's module system. The import, include, and modulemeta keywords are not supported. This accounts for 12 of the 17 skipped tests.
If you need to reuse filter logic across files, define functions inline or compose programs at the Java API level.
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 double range (> 2^53). For example:
# jq: 13911860366432393 - 10 => 13911860366432382
# jjq: 13911860366432393 - 10 => 13911860366432383
This affects 4 skipped tests. Normal-range integer and floating-point arithmetic works correctly.
fromjsonparse errors report a different column number than jq for certain invalid JSON (1 test)
See the User Guide for comprehensive documentation covering:
- CLI usage with examples
- Java library API with code samples
- Enterprise integration patterns (REST APIs, message queues, batch processing, metrics)
- Performance best practices
- jq language reference
This project is licensed under the Apache License 2.0.