Skip to content

Latest commit

 

History

303 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jjq

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.

Features

  • 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 supportjjq-yaml parses and emits YAML via SnakeYAML, with optional jjq-mapper integration for YAML → POJO mapping
  • Record and POJO data bindingjjq-mapper maps JqValue to/from Java types with @JqInclude, @JqNaming, @JqConverter, and Jackson/JSON-B annotation bridges
  • Jakarta EE integrationjjq-jakarta module provides Hibernate persistence (BYTEA + JSONB), JPA AttributeConverter, JAX-RS body/param providers, and JSON-B serializers
  • Jackson Modulejjq-jackson includes JqValueModule for native JqValue serialization in POJOs via ObjectMapper
  • Jackson migration compatpath(), 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

Modules

Module Description
jjq-core Lexer, parser, AST, bytecode VM, builtins (zero external dependencies)
jjq-jackson Jackson integration — JsonNodeJqValue 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

Quick Start

Maven

<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>

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.applyAll(input);
results.forEach(r -> System.out.println(r.toJsonString()));
// {"name":"Alice","email":"alice@example.com"}
// {"name":"Bob","email":"bob@example.com"}

Parse from byte[] (fastest path)

// Parse directly from bytes — no intermediate String allocation
byte[] jsonBytes = Files.readAllBytes(Path.of("data.json"));
JqValue data = JqValues.parse(jsonBytes);

Serialize to byte[] (zero-copy path)

// 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.

Navigate and extract values

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 from InputStream

// Parse directly from an InputStream (reads all bytes, then parses)
try (InputStream in = connection.getInputStream()) {
    JqValue data = JqValues.parse(in);
}

Pretty-print JSON

// 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
// }

Convert between JqValue and Java types

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-backed

Type structure (schema inference)

Compute 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 unioned

Build JSON values

import 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");

Jackson integration

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.

JSONata support (jjq-jsonata)

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.

Jakarta EE integration (jjq-jakarta)

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.

With variables

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);

Processing multiple inputs (JSONL)

List<JqValue> inputs = JqValues.parseAll("""
    {"name":"Alice","age":30}
    {"name":"Bob","age":25}
    """);

JqProgram program = JqProgram.compile(".name");
List<JqValue> names = program.applyAll(inputs);

Program introspection

// 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");  // false

CLI Usage

jjq [OPTIONS] FILTER [FILE...]

Options

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)

Examples

# 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"

Performance

JSON Parsing: jjq vs Jackson 3

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.

Production Query Execution on 14MB 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.

Record Data Binding: jjq-mapper vs Jackson 3

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.

JSON Serialization: jjq vs Jackson 3

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).

CLI Performance: jjq native-image vs jq 1.8.1

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.

Architecture

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)

Bytecode VM

  • 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

Value System

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.

Parser Optimizations

  • 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/double without BigDecimal or substring() 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.

Building

# 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

Supported jq Features

  • 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-end
  • try-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)

Known Limitations

jjq passes 491 of 508 upstream jq tests (96.7%). The remaining differences:

Module system (import / include / modulemeta)

jjq does not implement jq's module system. The import, include, and modulemeta keywords are not supported (12 skipped tests).

Big integer precision

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.

Minor error message differences

fromjson parse errors report a different column number than jq for certain invalid JSON (1 skipped test).

Documentation

License

This project is licensed under the Apache License 2.0.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages