Developer documentation for contributors.
User documentation: See DOCS.md
jsonq is a lightweight JSON query engine that implements a custom expression language for querying and transforming JSON data. The library provides a functional programming interface with method chaining, similar to JSONPath but with richer expression support.
Tokenizer (tokenizePipeline)
- Parses query strings into token streams
- Handles root reference (
$), property access (.prop), array indexing ([0]), and method calls (.method()) - Tracks token positions for extracting method parameters
Evaluator (evaluatePipeline)
- Executes token pipeline against data
- Maintains current value through transformations
- Handles nested data navigation
Expression Parser (parseExpr)
- Recursive descent parser for expressions
- Generates AST with nodes: literal, variable, property, arrayIndex, unary, binary, methodCall
- Supports operators: arithmetic, comparison, logical
Expression Evaluator (evalExpr, evalNode)
- Detects arrow function syntax with regex
- Creates variable bindings for named parameters
- Evaluates AST nodes with context (data, item, index, bindings)
Method Executor (applyMethod)
- Switch statement dispatching to method implementations
- Propagates bindings through nested method calls
- Handles recursive evaluation for nested transformations
export { query };query(expression, data)
- Main entry point
- Returns transformed data based on expression
npm install
npm testnpm testTest suite includes 98 tests covering:
- Tokenization and parsing
- All methods (map, filter, sort, etc.)
- Expression evaluation (arithmetic, logical, comparison)
- Nested method calls
- Arrow function syntax and underscore shorthand
- Root ($) and array ($array) variable access
- Edge cases and error handling
npm run buildOutputs to dist/:
index.cjs- CommonJSindex.mjs- ES Moduleindex.umd.js- UMD (browser)index.d.ts- TypeScript definitions
const METHODS = [
"map",
"filter",
"join",
"sort",
"unique",
"first",
"last",
"count",
"sum",
"avg",
"min",
"max",
];root-$referenceproperty-.propaccessarrayIndex-[0]accessmethod-.method()call with params
Variable syntaxes:
- Root reference:
$ - Built-in variables:
$item,$index,$array - Underscore shorthand:
_(alias for$item) - Arrow functions:
x => x.prop(creates binding forx)
Arrow function detection:
const arrowMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=>\s*(.+)$/);Binding propagation: Arrow function bindings are passed through nested method calls, enabling parent scope access in nested iterations.
literal- Numbers, strings, booleans, null, undefinedvariable-$,$item,$index,$array,_, or named paramsproperty- Property access (obj.prop)arrayIndex- Array indexing (arr[0])unary- Unary operators (!,-)binary- Binary operators (arithmetic, comparison, logical)methodCall- Nested method invocations
Descending sort detected by - prefix:
const descending = expr.startsWith("-");
const sortExpr = descending ? expr.substring(1) : expr;Dates prefixed with @ are parsed to timestamps:
if (token.startsWith("@")) {
return new Date(token.substring(1)).getTime();
}- No comments in implementation code
- Variable names provide documentation
- Concise, functional style
- Manual parsing (no external parser libraries)
- Add method name to
METHODSarray - Add case to
applyMethodswitch statement - Propagate
bindingsparameter for nested support - Add tests to
index.test.js - Update DOCS.md with user-facing documentation
- Update
tokenizeExprregex to include new operator - Add operator precedence in
parseExpr - Handle in
evalNodebinary operator cases - Add tests
- Test each method with basic usage
- Test chaining combinations
- Test edge cases (empty arrays, null values)
- Test error conditions
- Test nested method scenarios
- Test all three syntax styles (
$item,_, arrow functions)
├── src/
│ ├── index.js # Main implementation
│ └── cli.js # CLI tool (if applicable)
├── test/
│ └── index.test.js # Test suite
├── examples/
│ └── example.js # Usage examples
├── dist/ # Build outputs (generated)
├── DOCS.md # User documentation
├── README.md # Developer documentation (this file)
├── LICENSE # MIT License
├── .gitignore
├── package.json
├── rollup.config.mjs
└── jest.config.js
We use a manual tokenizer/parser instead of a library to:
- Keep bundle size minimal
- Avoid external dependencies
- Maintain full control over syntax
- Optimize for our specific use case
Supporting $item, _, and arrow functions provides:
- Backward compatibility (
$item) - Conciseness (
_) - Clarity and parent scope access (arrow functions)
The > operator was removed to avoid:
- Confusion with greater-than comparisons
- Additional parsing complexity
- Preference for dot-chaining which is more familiar
All operations chain with .method() syntax for consistency with JavaScript's native array methods.
- No AST caching (expressions evaluated fresh each time)
- Recursive descent parser (adequate for expression complexity)
- Methods execute eagerly (no lazy evaluation)
- Suitable for small to medium datasets
- No async support
- No custom function definitions
- Single-parameter arrow functions only
- No array/object literal construction in expressions
- Limited to JavaScript-compatible JSON data
- Create a branch with the appropriate naming convention (see below)
- Make your changes
- Run tests:
npm test - Build:
npm run build - Commit changes
- Create a pull request to
main - Once merged, the GitHub Actions workflow will automatically:
- Bump the version based on branch name
- Build the package
- Publish to npm with provenance
The automated release workflow determines the version bump based on your branch name:
- Patch release (1.0.0 → 1.0.1): Use
fix/*orpatch/*- Example:
fix/cli-error-handling,patch/typo-in-docs
- Example:
- Minor release (1.0.0 → 1.1.0): Use
add/*orminor/*- Example:
add/new-filter-method,minor/improve-performance
- Example:
- Major release (1.0.0 → 2.0.0): Use
vnext/*ormajor/*- Example:
vnext/breaking-api-change,major/remove-deprecated-methods
- Example:
If your branch name doesn't match any pattern, it defaults to a major version bump.
MIT query("$.users.map($item.name)", data);
// Arithmetic query("$.items.map(price * 1.1)", data); query("$.items.map(_.price * 1.1)", data);
// Complex expressions query("$.users.map(u => u.age * 2 + 10)", data);
### filter(condition)
Select elements matching a condition.
```javascript
// Comparison operators
query("$.users.filter(age > 25)", data);
query('$.users.filter(name == "Alice")', data);
// Using underscore
query("$.users.filter(_.age > 25)", data);
// Using arrow syntax
query("$.users.filter(u => u.age > 25 && u.active)", data);
// Logical operators
query("$.users.filter(age > 25 && active == true)", data);
query("$.users.filter(age < 20 || age > 60)", data);
// Negation
query("$.users.filter(!active)", data);
query("$.users.filter(!_.deleted)", data);
Sort array elements. Use negative expression for descending order.
// Sort ascending (default)
query("$.users.sort(age)", data);
query("$.users.sort(name)", data);
// Sort descending (use negative)
query("$.users.sort(-age)", data);
query("$.users.sort(-name)", data);
// Sort with expressions
query("$.items.sort(price * quantity)", data);
query("$.items.sort(-(price * quantity))", data); // DescendingJoin array elements into a string.
query('$.tags.join(", ")', data);
query('$.users.map(name).join(" | ")', data);Get unique values from an array.
// Unique primitives
query("$.tags.unique($item)", data);
// Unique by property
query("$.users.unique(department)", data);Get the first element.
query("$.users.first()", data);
query("$.users.filter(active).first()", data);Get the last element.
query("$.users.last()", data);
query("$.users.sort(age).last()", data);Count array elements.
query("$.users.count()", data);
query("$.users.filter(active).count()", data);Sum values in an array.
query("$.items.sum(price)", data);
query("$.numbers.sum($item)", data);Calculate average of values.
query("$.users.avg(age)", data);
query("$.items.avg(price * quantity)", data);Find minimum value.
query("$.users.min(age)", data);
query("$.items.min(price)", data);Find maximum value.
query("$.users.max(age)", data);
query("$.items.max(price)", data);Built-in variables:
$item- Current element in iteration$index- Current index in iteration$data- Root data object_- Shorthand for$item(concise syntax)
Arrow function syntax: Use arrow functions to name your parameters:
// Named parameter (any name you want)
query("$.users.map(user => user.name)", data);
query("$.users.filter(person => person.age > 25)", data);
// Access parent scope in nested iterations
query(
'$.departments.map(dept => dept.employees.map(emp => dept.name + ": " + emp.name))',
data,
);Examples:
// Using built-in variables
query("$.users.map($item.name)", data);
query("$.users.map($index)", data);
query("$.users.filter($item.age > $data.minAge)", data);
// Using underscore shorthand (more concise)
query("$.users.map(_.name)", data);
query("$.items.filter(_.price < 100)", data);
// Using arrow syntax for clarity
query("$.users.map(u => u.name)", data);
query("$.items.filter(item => item.price < 100)", data);Nested references:
// Underscore shadows in nested contexts
query("$.departments.map(_.employees.map(_.name))", data);
// Outer _ = department, inner _ = employee (shadowed)
// Use arrow syntax when you need parent access
query(
'$.departments.map(dept => dept.employees.map(emp => dept.name + ": " + emp.name))',
data,
);
// Mix both styles
query("$.groups.map(g => g.items.filter(_.value > 10).map(_.name))", data);Arithmetic: +, -, *, /, %
query("$.items.map(price * 1.2)", data);
query("$.users.filter(age % 2 == 0)", data);Comparison: ==, !=, <, >, <=, >=
query("$.users.filter(age >= 18)", data);
query("$.items.filter(stock != 0)", data);Logical: &&, ||, !
query("$.users.filter(age > 18 && active == true)", data);
query("$.items.filter(inStock || onOrder)", data);
query("$.users.filter(!deleted)", data);- Numbers:
42,3.14,-10,1.5e10 - Strings:
"hello",'world' - Booleans:
true,false - Null:
null - Undefined:
undefined
query("$.users.filter(age > 25)", data);
query('$.users.filter(name == "Alice")', data);
query("$.users.filter(deleted == null)", data);Use @ prefix for ISO date strings:
const data = [
{ event: "Launch", date: "2024-01-15" },
{ event: "Update", date: "2024-06-20" },
];
query("filter(date > @2024-03-01).map(event)", data);
// => ['Update']
// With timestamps
query("filter(createdAt > @2024-01-01T12:00:00Z)", data);Access nested properties in expressions:
query("$.users.map($item.address.city)", data);
query("$.users.map(_.address.city)", data);
query("$.users.map(u => u.address.city)", data);
query("$.items.filter($item.specs.weight > 100)", data);Use brackets in expressions:
query("$.users.map($item.tags[0])", data);
query("$.users.map(_.tags[0])", data);
query("$.data.map($item.values[$index])", data);Call methods within expressions for complex transformations:
const data = {
departments: [
{
name: "Engineering",
employees: [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Carol", age: 35 },
],
},
{
name: "Sales",
employees: [
{ name: "Dave", age: 40 },
{ name: "Eve", age: 28 },
],
},
],
};
// Filter and map employees within each department (concise)
query("$.departments.map(_.employees.filter(age > 28).map(name))", data);
// => [['Alice', 'Carol'], ['Dave']]
// With arrow syntax for parent access
query(
'$.departments.map(dept => dept.employees.map(emp => dept.name + ": " + emp.name))',
data,
);
// => [['Engineering: Alice', 'Engineering: Bob', 'Engineering: Carol'], ['Sales: Dave', 'Sales: Eve']]
// Count employees per department
query("$.departments.map(_.employees.count())", data);
// => [3, 2]
// Average age per department
query("$.departments.map(_.employees.avg(age))", data);
// => [30, 34]
// Multiple levels of nesting with underscore
query("$.departments.map(_.employees.sort(age).first().name)", data);
// => ['Bob', 'Eve']const orders = {
items: [
{ name: "Laptop", price: 999, quantity: 2, inStock: true },
{ name: "Mouse", price: 25, quantity: 5, inStock: true },
{ name: "Keyboard", price: 75, quantity: 0, inStock: false },
],
};
// Total value of in-stock items
query("$.items.filter(inStock).map(price * quantity).sum($item)", orders);
// => 2048
// Most expensive item name
query("$.items.sort(price).last().name", orders);
// => 'Laptop'
// Average price
query("$.items.avg(price)", orders);
// => 366.33...const company = {
departments: [
{
name: "Engineering",
employees: [
{ name: "Alice", salary: 120000, startDate: "2020-01-15" },
{ name: "Bob", salary: 95000, startDate: "2021-06-01" },
],
},
{
name: "Sales",
employees: [{ name: "Carol", salary: 85000, startDate: "2019-03-10" }],
},
],
};
// All department names
query('$.departments.map(name).join(", ")', company);
// => 'Engineering, Sales'
// High earners in each department
query(
"$.departments.map($item.employees.filter(salary > 100000).map(name))",
company,
);
// => [['Alice'], []]
// Recent hires
query(
"$.departments[0].employees.filter(startDate > @2021-01-01).map(name)",
company,
);
// => ['Bob']
// Department sizes
query("$.departments.map($item.employees.count())", company);
// => [2, 1]const metrics = {
daily: [
{ date: "2024-01-01", views: 1200, clicks: 45 },
{ date: "2024-01-02", views: 1500, clicks: 67 },
{ date: "2024-01-03", views: 980, clicks: 32 },
],
};
// Click-through rates
query("$.daily.map(clicks / views * 100)", metrics);
// => [3.75, 4.47, 3.27]
// Best performing day
query("$.daily.sort(clicks).last().date", metrics);
// => '2024-01-02'
// Total engagement
query("$.daily.sum(views + clicks)", metrics);
// => 3824Execute a query against data.
Parameters:
expression(string): Query expressiondata(any): Data to query
Returns: Query result
const result = query("$.users.filter(age > 25)", data);Parse expression into tokens (exported for testing/debugging).
Parameters:
expression(string): Query expression
Returns: Array of tokens
import { tokenize } from "jsonq";
const tokens = tokenize("$.users.map(name)");Evaluate parsed tokens against data (exported for testing/debugging).
Parameters:
tokens(Array): Parsed tokensdata(any): Data to evaluate
Returns: Evaluation result
import { evaluate, tokenize } from "jsonq";
const tokens = tokenize("$.users.count()");
const result = evaluate(tokens, data);Combine multiple conditions:
query(
`
$.users
.filter(age >= 21 && age <= 65)
.filter(active == true)
.filter(verified == true)
.sort(name)
`,
data,
);Access deeply nested data:
query('$.company.departments[0].teams[2].members.filter(role == "lead")', data);Combine nested methods with property navigation:
// Get top performer from each department
query("$.departments.map($item.employees.sort(performance).last())", data);
// Filter departments by employee criteria
query(
"$.departments.filter($item.employees.filter(certified == true).count() > 5)",
data,
);
// Aggregate across multiple levels
query("$.regions.map($item.stores.map($item.sales.sum(amount)))", data);Use expressions in any method:
query("$.products.filter((price - cost) / price > 0.3)", data);Use $index for position-dependent logic:
query("$.items.filter($index % 2 == 0)", data); // Even indices
query("$.items.map($index * 10 + $item.value)", data);Safe navigation with optional chaining behavior:
const data = [{ user: { name: "Alice" } }, { user: null }, {}];
query("map($item.user.name)", data);
// => ['Alice', undefined, undefined]Accessing non-existent properties returns undefined:
query("$.users.map(nonexistent)", data);
// => [undefined, undefined, ...]Dates are automatically coerced for comparison:
query("filter(dateString > @2024-01-01)", data);-
Filter Early: Apply filters before expensive operations
// Good query("$.users.filter(active).map(expensiveOperation)", data); // Less efficient query("$.users.map(expensiveOperation).filter(active)", data);
-
Avoid Redundant Sorts: Sort only when necessary
// If you only need first/last, consider avoiding sort query("$.users.sort(age).first()", data);
-
Use Specific Expressions: More specific filters run faster
// More specific query("$.users.filter(id == 123)", data); // Less specific query("$.users.filter(id > 0)", data);
The library throws errors for:
- Unknown methods
- Invalid expressions
- Unknown variables
- Unexpected tokens
- Invalid literals
try {
query("$.users.unknownMethod()", data);
} catch (error) {
console.error(error.message); // "Unknown method: unknownMethod"
}Run the test suite:
npm testMIT