High-performance pure Java jq implementation with a bytecode-compiled VM, deferred string parsing, and zero-allocation query execution on large documents.
jjq provides a complete jq filter engine with zero native dependencies, making it portable across all JVM platforms. It executes field access queries in 3 nanoseconds with zero allocation on a 14MB production document, parses 1.5-2.3x faster than Jackson 3, and serializes 1.8x faster.
- 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 — fused iteration opcodes, whole-program shape detection, constant folding, peephole optimization, pre-allocated stacks
- Fast JSON parsing — direct digit accumulation, deferred string values, byte[]-based parsing, field name interning with hash mixing. 1.5-2.3x faster than Jackson 3 on 1MB inputs; 1.5x faster on 14MB production data
- Zero-allocation queries — field access, deep field chains, keys, and length on pre-parsed documents produce zero garbage
- Thread-safe — compiled programs are immutable and can be shared across threads
- YAML support —
jjq-yamlparses and emits YAML via SnakeYAML, with optionaljjq-mapperintegration for YAML → POJO mapping - Record and POJO data binding —
jjq-mappermapsJqValueto/from Java types with@JqInclude,@JqNaming,@JqConverter, and Jackson/JSON-B annotation bridges - Jakarta EE integration —
jjq-jakartamodule provides Hibernate persistence (BYTEA + JSONB), JPAAttributeConverter, JAX-RS body/param providers, and JSON-B serializers - Jackson Module —
jjq-jacksonincludesJqValueModulefor nativeJqValueserialization in POJOs viaObjectMapper - Jackson migration compat —
path(),isMissingNode(),asText(default)aliases; nested builders (putObject/putArray); annotation bridges for transparent adoption - Multiple JSON adapters — Jackson, fastjson2, and byte[] adapters with lazy zero-copy conversion
- Java 21+ — leverages sealed classes, records, and pattern matching
| Module | Description |
|---|---|
jjq-core |
Lexer, parser, AST, bytecode VM, builtins (zero external dependencies) |
jjq-jackson |
Jackson integration — JsonNode ↔ JqValue conversion, JqValueModule for native POJO serialization |
jjq-fastjson2 |
fastjson2 adapter with lazy conversion and streaming APIs |
jjq-jakarta |
Hibernate persistence, JPA AttributeConverter, JAX-RS providers, ParamConverter, and JSON-B serializers |
jjq-mapper |
Record and POJO data binding — map JqValue to/from Java types with @JqField, @JqInclude, @JqNaming, @JqConverter |
jjq-mapper-processor |
Compile-time annotation processor for jjq-mapper — generates optimized mappings (6-11x faster than Jackson 3) |
jjq-mapper-jackson |
Bridges Jackson 2 annotations (@JsonProperty, @JsonIgnore, @JsonInclude) into jjq-mapper |
jjq-mapper-jsonb |
Bridges JSON-B annotations (@JsonbProperty, @JsonbTransient, @JsonbNillable) into jjq-mapper |
jjq-yaml |
YAML parsing into JqValue trees via SnakeYAML — enables jq queries over YAML documents |
jjq-jsonata |
Compile-time JSONata-to-jq transpiler — 468/1219 conformance tests passing |
jjq-jsonpath |
SQL/JSON path to jq converter with lax/strict mode support — 123/558 conformance |
jjq-cli |
Command-line interface (zero dependencies, GraalVM native-image ready) |
jjq-test-suite |
466 conformance tests + 525 upstream jq tests (96.0% passing) |
jjq-benchmark |
JMH benchmarks vs Jackson 3.2.2: parsing, serialization, mapper, allocation profiling |
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-core</artifactId>
<version>0.1.4-SNAPSHOT</version>
</dependency>
<!-- For Jackson integration -->
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-jackson</artifactId>
<version>0.1.4-SNAPSHOT</version>
</dependency>
<!-- For Hibernate/JAX-RS integration -->
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-jakarta</artifactId>
<version>0.1.4-SNAPSHOT</version>
</dependency>
<!-- For JSONata support -->
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-jsonata</artifactId>
<version>0.1.4-SNAPSHOT</version>
</dependency>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.applyAll(input);
results.forEach(r -> System.out.println(r.toJsonString()));
// {"name":"Alice","email":"alice@example.com"}
// {"name":"Bob","email":"bob@example.com"}// Parse directly from bytes — no intermediate String allocation
byte[] jsonBytes = Files.readAllBytes(Path.of("data.json"));
JqValue data = JqValues.parse(jsonBytes);// Serialize directly to UTF-8 bytes — no intermediate String allocation
byte[] output = JqValues.serializeToBytes(data);
// Also works with OutputStream (uses the byte path internally)
JqValues.serializeTo(data, outputStream);For documents parsed from byte[], deferred string values are copied as raw bytes without constructing Java Strings or re-encoding UTF-8. This makes the parse(byte[]) -> serializeToBytes() round-trip optimal for pass-through workloads like database persistence and message queues.
For repeated queries, always use JqProgram.compile() -- it compiles once and executes in nanoseconds:
// Compile once at startup (thread-safe, reusable)
JqProgram getName = JqProgram.compile(".user");
JqProgram getResults = JqProgram.compile(".autobench_workload.data[0].results");
// Apply many times (3 ns per field access, zero allocation)
JqValue user = getName.apply(data);
JqValue results = getResults.apply(data);For programmatic navigation (traversing results, iterating dynamic field names), use the null-safe convenience methods:
// Null-safe chaining -- returns JqNull.NULL for missing paths or type mismatches
JqValue results = data.getField("workload").getField("data").getElement(0).getField("results");
// JSON Pointer navigation (RFC 6901)
JqValue results = data.at("/workload/data/0/results");
// Safe value extraction with defaults
String name = data.getField("user").asString("unknown");
long count = data.getField("count").asLong(0);
int page = data.getField("page").asInt(1);
double score = data.getField("score").asDouble(0.0);
// Coercing extractors -- extract numbers from JqNumber or numeric strings
Double value = data.getField("metric").tryDouble(); // null if not numeric
Long id = data.getField("id").tryLong(); // null if not parseable
Integer port = data.getField("port").tryInt(); // null if not parseable
// Fail-fast access -- throws JqTypeError if missing
JqValue required = data.required("user"); // throws if missing
JqValue element = data.required(0); // throws if out of bounds
// Type checks
data.isNull(); data.isString();
data.isNumber(); data.isBoolean();
data.isArray(); data.isObject();
data.isIntegralNumber(); data.isFloatingPointNumber();
// Check existence
if (data.has("user") && !data.getField("items").isEmpty()) {
// ...
}
// Iterate arrays directly (JqArray implements Iterable<JqValue>)
JqArray items = (JqArray) data.getField("items");
for (JqValue item : items) {
System.out.println(item.getField("name").asText());
}
// Stream support
List<String> names = items.stream()
.filter(item -> item.getField("active").asBoolean(false))
.map(item -> item.getField("name").asText())
.toList();
// Object accessors — forEach avoids Map.Entry allocation for array-backed objects
JqObject config = (JqObject) data.getField("config");
config.forEach((key, val) -> System.out.println(key + "=" + val));
for (String key : config.keys()) { /* ... */ }
for (var entry : config.entries()) { /* ... */ }// Parse directly from an InputStream (reads all bytes, then parses)
try (InputStream in = connection.getInputStream()) {
JqValue data = JqValues.parse(in);
}// Compact (default)
String compact = value.toJsonString(); // {"name":"Alice","age":30}
// Pretty-printed with 2-space indentation
String pretty = JqValues.toPrettyJsonString(value);
// {
// "name": "Alice",
// "age": 30
// }Recursive conversion for integrating with libraries that operate on Map/List/primitives (JSONata engines, GraalVM polyglot, JDBC, template engines):
// Java → JqValue (null → JqNull.NULL, Map → JqObject, List → JqArray, etc.)
Map<String, Object> javaMap = Map.of("name", "Alice", "scores", List.of(1, 2, 3));
JqValue value = JqValues.fromJavaObject(javaMap);
// JqValue → Java (JqNull → null, JqObject → LinkedHashMap, JqArray → ArrayList, etc.)
Object javaObj = value.toJavaObject();
// JqNumber.of(Number) — accepts any Number subtype (Integer, Long, Double, Float, BigDecimal, etc.)
JqNumber n = JqNumber.of(someNumber); // auto-promotes integral types to long-backedCompute a type skeleton from a JSON document — replaces leaf values with their type names while preserving the object/array structure. Useful for schema inference, data profiling, and structural comparison:
JqValue input = JqValues.parse("""
{"name": "Alice", "age": 30, "scores": [95, 87.5]}
""");
JqValue schema = JqValues.typeStructure(input);
// {"name":"string","age":"integer","scores":["number"]}Type mapping: null -> "null", boolean -> "boolean", integral number -> "integer", floating-point -> "number", string -> "string". Arrays merge element schemas into a single representative.
Merge schemas from multiple documents to build a unified type structure:
JqValue doc1 = JqValues.parse("{\"value\": 42}");
JqValue doc2 = JqValues.parse("{\"value\": 3.14, \"extra\": true}");
JqValue merged = JqValues.mergeTypeStructures(
JqValues.typeStructure(doc1),
JqValues.typeStructure(doc2));
// {"value":"number","extra":"boolean"}
// "integer" + "number" promoted to "number"; keys unionedimport io.hyperfoil.tools.jjq.value.*;
// Object builder with type-safe convenience methods
JqObject result = JqObject.builder()
.put("name", "Alice")
.put("age", 30)
.put("score", 95.5)
.put("active", true)
.put("data", someJqValue) // null is treated as JqNull.NULL
.build();
// Array builder
JqArray items = JqArray.arrayBuilder()
.add("first")
.add("second")
.add(42)
.build();
// Quick object construction with auto-wrapped values (String, Number, Boolean, null)
JqObject block = JqObject.ofEntries(
"type", "header",
"count", 42,
"active", true,
"nested", JqObject.ofEntries("key", "value")
);
// Copy-on-write modification (immutable — returns new instances)
JqObject updated = result.with("status", JqString.of("verified"));
JqObject merged = result.merge(otherObject);
JqObject deepMerged = result.deepMerge(otherObject); // recursive merge for nested objects
JqObject without = result.without("age");Register JqValueModule on an ObjectMapper to natively serialize/deserialize JqValue fields
in POJOs — no manual conversion needed:
import com.fasterxml.jackson.databind.ObjectMapper;
import io.hyperfoil.tools.jjq.jackson.JqValueModule;
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JqValueModule());
// POJOs with JqValue fields work automatically
record Config(String name, JqValue settings) {}
String json = mapper.writeValueAsString(new Config("prod", myJqValue));
Config restored = mapper.readValue(json, Config.class);Use JacksonJqEngine to apply jq filters to Jackson JsonNode trees:
import io.hyperfoil.tools.jjq.jackson.JacksonJqEngine;
JacksonJqEngine engine = new JacksonJqEngine(mapper);
JqProgram program = engine.compile(".users[] | {name, email}");
JsonNode input = mapper.readTree(requestBody);
List<JsonNode> results = engine.apply(program, input);See the jjq-jackson README for full details on lazy conversion, Spring Boot, and Quarkus integration.
Run JSONata expressions through jjq's optimized bytecode VM. The transpiler converts JSONata to jq at compile time — zero runtime overhead:
import io.hyperfoil.tools.jjq.jsonata.JsonataCompiler;
// Compile JSONata to jq (one-time cost, ~microseconds)
JqProgram program = JsonataCompiler.compile("$sum(orders.price)");
// Execute through jjq's bytecode VM — same performance as native jq
JqValue result = program.apply(data);Supports navigation, operators, predicates, 35+ built-in functions, implicit array mapping, variable binding, lambdas ($map/$filter/$reduce), ~> pipe operator, ** recursive descent, and more. See the jjq-jsonata README for full details and conformance status.
Use JqValue directly as a Hibernate entity field type, in REST endpoints, and with JSON-B.
Hibernate persistence — BYTEA (recommended) or JSONB columns:
@Entity
public class MyEntity {
// Option 1: BYTEA — zero-copy byte[] I/O (recommended)
@JqValueColumn
@Column(columnDefinition = "BYTEA")
public JqValue data;
// Option 2: Portable JPA — works with any JPA provider
@Convert(converter = JqValueConverter.class)
@Column(columnDefinition = "TEXT")
public JqValue metadata;
}JAX-RS providers — auto-discovered via @Provider:
@POST @Path("/upload")
public Response upload(JqValue data) { // MessageBodyReader
service.store(data);
return Response.ok().build();
}
@GET @Path("/{id}")
public JqValue get(@PathParam("id") long id) { // MessageBodyWriter
return service.load(id);
}
@GET @Path("/search")
public Response search(@QueryParam("filter") JqValue filter) { // ParamConverter
return Response.ok(service.search(filter)).build();
}JSON-B support — for environments using JSON-B instead of Jackson:
JsonbConfig config = new JsonbConfig()
.withSerializers(new JqValueJsonbSerializer())
.withDeserializers(new JqValueJsonbDeserializer());
Jsonb jsonb = JsonbBuilder.create(config);See the jjq-jakarta README for full setup details, including Quarkus and Spring Boot configuration.
import io.hyperfoil.tools.jjq.evaluator.Environment;
JqProgram program = JqProgram.compile(".[] | select(.name == $target)");
Environment env = new Environment();
env.setVariable("target", JqString.of("Alice"));
List<JqValue> results = program.applyAll(input, env);List<JqValue> inputs = JqValues.parseAll("""
{"name":"Alice","age":30}
{"name":"Bob","age":25}
""");
JqProgram program = JqProgram.compile(".name");
List<JqValue> names = program.applyAll(inputs);// Detect if a program needs --null-input mode (uses input/inputs builtins)
JqProgram program = JqProgram.compile("[inputs | .name]");
if (program.usesNullInput()) {
results = program.applyNullInput(sourceValues);
} else {
results = program.applyAll(input);
}
// Check if a program references any specific builtin
program.referencesBuiltin("inputs"); // true
program.referencesBuiltin("length"); // falsejjq [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 |
--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
# Process JSONL (one JSON value per line)
printf '{"name":"Alice"}\n{"name":"Bob"}\n' | jjq '.name'
# "Alice"
# "Bob"Parsing throughput on byte[] inputs (ms/op, lower is better). Measured with JMH, 3 forks, 5+5 iterations, JDK 25.0.2 Temurin. Jackson version: 3.2.2.
| Input | jjq (byte[]) | Jackson 3 (byte[]) | jjq speedup |
|---|---|---|---|
| flat 1MB | 1.57 ms | 2.72 ms | 1.7x |
| strings 1MB | 0.81 ms | 1.83 ms | 2.3x |
| numbers 1MB | 1.84 ms | 3.30 ms | 1.8x |
| nested 1MB | 1.70 ms | 2.66 ms | 1.6x |
| Production 14MB | 14.2 ms | 21.0 ms | 1.5x |
jjq's byte[] parser is 1.5-2.3x faster than Jackson 3 across all input types. The biggest win is on string-heavy JSON (2.3x) where deferred string construction avoids allocating Java Strings for untouched values. Field name interning with hash mixing enables zero-allocation queries on the parsed document.
Query execution on a pre-parsed 14MB production upload (351K nodes, 3,668 objects). Times via JqProgram.apply() — the h5m API path including compiled program dispatch:
| Query | Expression | Time | Allocation |
|---|---|---|---|
| Top-level field | .user |
3.1 ns | 0 B |
| Deep field (4 levels) | .autobench_workload.data[0].results |
63 ns | 0 B |
| Keys (127-key object) | .data[0].pcp_time_series[0] | keys |
69 ns | 0 B |
| Array length | .data[0].pcp_time_series | length |
64 ns | 0 B |
| PCP entry (127-key object) | .data[0].pcp_time_series[0] |
69 ns | 0 B |
| Object construction | {user, uuid, run_id, start_time, end_time} |
95 ns | 168 B |
| Config extract | .rhivos_config | {build, model, kernel, architecture} |
105 ns | 144 B |
| Iterate + extract | [.stressng_workload.data[] | .sample_uuid] |
150 ns | 80 B |
| Extract metric (502 entries) | [.pcp_time_series[] | .["mem.util.used"]] |
13 us | 2.1 KB |
| Round-trip (extract + serialize) | .user |
15 ns | 56 B |
Seven of fifteen production benchmarks achieve zero allocation per query — the result comes directly from the pre-parsed document with no object creation. Single field access bypasses the VM entirely via inlined fast path detection.
Deserialization from pre-parsed tree to Java records (ns/op, lower is better). JMH, 3 forks, 5+5 iterations, JDK 25.0.2 Temurin.
| Record type | jjq generated | jjq reflection | Jackson 3 | gen vs Jackson |
|---|---|---|---|---|
| simple (5 fields) | 22 ns | 128 ns | 227 ns | 10.3x |
| nested (record in record) | 33 ns | 206 ns | 364 ns | 11.2x |
End-to-end deserialization from byte[] to Java records:
| Record type | jjq (fromBytes) | Jackson 3 (fromBytes) | jjq speedup |
|---|---|---|---|
| simple | 252 ns | 362 ns | 1.4x |
| nested | 374 ns | 475 ns | 1.3x |
| list | 826 ns | 976 ns | 1.2x |
The compile-time annotation processor (jjq-mapper-processor) generates _JqMapping classes that map fields via direct enum-switch dispatch, avoiding reflection and lambda overhead. The generated mappings maintain a 6-11x advantage over Jackson 3 on pre-parsed data, narrowing to 2x at 20 fields due to register spilling on x86_64.
Serialization throughput (ms/op, lower is better). JMH, 3 forks, 5+5 iterations, JDK 25.0.2 Temurin.
| Input | jjq | Jackson 3 | jjq speedup |
|---|---|---|---|
| Production 14MB | 10.3 ms | 18.4 ms | 1.78x |
| flat 1MB | 1.79 ms | 2.05 ms | 1.14x |
| strings 1MB | 0.88 ms | 1.57 ms | 1.77x |
| numbers 1MB | 2.08 ms | 2.87 ms | 1.38x |
| nested 1MB | 1.47 ms | 2.38 ms | 1.61x |
jjq serializes 1.1-1.8x faster than Jackson 3 across all inputs. The speedup comes from pre-computed JSON key forms for interned field names (no escape scanning), type-specialized appendTo dispatch, and pre-sized StringBuilder buffers based on the source byte length (eliminates repeated buffer doubling for large documents).
GraalVM native-image comparison on the 14MB production file (hyperfine, best of 3+ warmup runs):
| Test | jq 1.8.1 | jjq (native) | Notes |
|---|---|---|---|
Startup ('.' /dev/null) |
1.4 ms | 1.7 ms | Both sub-2ms |
Field access (.user) |
122 ms | 168 ms | Parse-dominated |
Deep field (.a.b[0].c) |
122 ms | 168 ms | Parse-dominated |
| Object construction | 123 ms | 168 ms | Parse-dominated |
| Identity round-trip (parse + serialize) | 245 ms | 213 ms | jjq 15% faster |
For CLI one-shot usage, jq 1.8.1's C parser is ~36% faster on parse-dominated workloads. jjq wins on full round-trips (parse + serialize) due to faster serialization. The native-image binary is 15 MB vs jq's 36 KB.
jjq's strength is the library use case — parse once, query many times. In h5m, a 14MB upload is parsed once and queried with dozens of jq expressions. The interning, zero-allocation queries, and pre-compiled bytecode VM amortize across all queries, achieving 3 ns field access with zero garbage. This is not measurable in CLI one-shot benchmarks where parse time dominates.
jq expression string
|
[Lexer] Hand-written, keyword-aware
|
Token stream
|
[Parser] Pratt parser (top-down operator precedence)
|
AST (JqExpr) ~35 sealed record types
|
[Compiler] AST -> Bytecode with fusion + folding
|
[VirtualMachine] Stack-based, FORK/BACKTRACK for generators
|
Output (JqValue)
- 74 opcodes with fused iteration (COLLECT_ITERATE, REDUCE_ITERATE, COLLECT_SELECT_ITERATE)
- 21 inlined builtin opcodes (length, type, keys, sort, add, etc.)
- Compound instructions (DOT_FIELD2 for
.a.b, BUILD_OBJECT with pre-computed layouts) - Whole-program shape detection — IDENTITY, FIELD_ACCESS, FIELD_ACCESS2, PIPE_FIELD_ARITH, BUILTIN bypass the VM loop entirely
- Constant folding and peephole optimization at compile time
- Pre-allocated growable stacks with pre-ensured capacity for hot loops
All JqValue types implement Serializable for Hibernate second-level cache support. Singletons (JqNull.NULL, JqBoolean.TRUE/FALSE, cached JqNumber instances) preserve identity across serialization via readResolve().
| Type | Implementation | Key optimization |
|---|---|---|
JqNull |
Singleton | Zero allocation |
JqBoolean |
TRUE/FALSE constants | Zero allocation |
JqNumber |
long fast-path with cache [-128, 1023], double for decimals, BigDecimal fallback. of(Number) accepts any Number subtype. |
Direct digit accumulation avoids new BigDecimal() for 99% of numbers |
JqString |
Deferred: holds (source, start, end) reference, materializes lazily. Serialization proxy materializes before writing. |
Zero-copy serialization via sb.append(source, start, end) for untouched strings |
JqArray |
List<JqValue> via raw JqValue[] |
ofTrusted() avoids defensive copying |
JqObject |
Parallel String[] keys + JqValue[] values, Builder for zero-intermediate construction. Serialization proxy converts map-backed to array-backed. |
Linear scan for ≤32 keys, hash index for larger objects. Interned field names enable reference equality. Pre-computed "key": JSON form eliminates escapeJson scanning. Copy-on-write with()/without()/merge()/deepMerge(). forEach(BiConsumer) for zero-allocation iteration. |
- byte[]-based parser (
JqValues.parse(byte[])) — parses UTF-8 bytes directly, no intermediate String - SWAR scanning — finds
"and\in 8 bytes per iteration using Netty-style bit manipulation - Deferred string values — string values hold source references, materialized only when accessed
- Field name interning — open-addressing hash table (1024 slots, 4-probe linear probing) with fused SWAR+hash computation. Quad-based cache verification (1-3 int comparisons for keys <=12 bytes) with hash fast-reject. Cache hits return the same String instance without
substring(). Pre-computed"key":JSON form eliminates escape scanning during serialization. - Direct digit accumulation — integers and decimals parsed to
long/doublewithoutBigDecimalorsubstring()for numbers with ≤15 significant digits - Thread-local buffer reuse — both char-based (StringBuilder) and byte-based (BytOutput) serialization buffers grow once and are reused across calls
- Direct byte serialization (
JqValues.serializeToBytes(byte[])) — serializes directly to UTF-8 bytes without intermediate String. Deferred-bytes strings copy raw source bytes (zero encoding). Interned field names use pre-computed byte forms. Numbers serialize directly to ASCII digits.
# 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
# Run benchmarks
mvn package -pl jjq-core,jjq-jackson,jjq-fastjson2,jjq-benchmark -DskipTests
java --enable-preview -jar jjq-benchmark/target/jjq-benchmark-0.1.4-SNAPSHOT.jar
# Run specific benchmark class
./scripts/run-benchmarks.sh JsonParseComparisonBenchmark
# Run with allocation profiling
java --enable-preview -jar jjq-benchmark/target/jjq-benchmark-0.1.4-SNAPSHOT.jar \
JsonProductionBenchmark -prof gc -rf json -rff results.json- 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 label-break- Assignment operators (
|=,+=,-=,*=,/=,%=,//=) - Path expressions (
path(),getpath,setpath,delpaths,del) - 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:
jjq does not implement jq's module system. The import, include, and modulemeta keywords are not supported (12 skipped tests).
jq uses arbitrary-precision integers internally. jjq uses long with BigDecimal fallback, which can produce slightly different results for integers beyond 2^53 (4 skipped tests). Normal-range arithmetic works correctly.
fromjson parse errors report a different column number than jq for certain invalid JSON (1 skipped test).
- User Guide — CLI usage, Java API, integration patterns
- Performance Guide — benchmarking methodology, profiling, optimization history
- Profiling Guide — JMH profiler recipes, async-profiler flame graphs
- jjq-jackson README — Jackson Module, JacksonJqEngine, JsonNode conversion
- jjq-jakarta README — Hibernate, JPA, JAX-RS, and JSON-B integration
- jjq-mapper README — Record data binding, @JqField, type support
- jjq-mapper-processor README — Compile-time code generation, setup, benchmarks
This project is licensed under the Apache License 2.0.