JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 128
  • Score
    100M100P100Q94267F
  • License MIT

Query JSON data with compact string expressions. Filter collections, transform values, evaluate conditions, and reuse variables across queries without adding dependencies.

Package Exports

  • @nanotiny/json-expression

Readme

@nanotiny/json-expression

Query JSON data with compact string expressions. Filter collections, transform values, evaluate conditions, and reuse variables across queries without adding dependencies.

Beta: This package is still in beta. Expect the API and query behavior to evolve, and be aware that breaking changes may happen between releases until the project reaches a stable version.

Install

npm install @nanotiny/json-expression

Highlights

  • Zero runtime dependencies
  • Works in Node.js and modern browsers
  • Supports filtering, transforms, and boolean evaluation
  • Includes shared query context for variables and aliases
  • Ships with TypeScript declarations, ESM, and CommonJS builds

Quick Start

import {
  QueryContext,
  evaluate,
  query,
  queryAsArray,
  queryBatch,
} from '@nanotiny/json-expression';

const data = {
  version: '2.0',
  status: 'active',
  companies: [
    { id: 'company1', name: 'Acme Corporation', type: 'corporation', revenue: 15000000 },
    { id: 'company2', name: 'Tech Solutions Ltd', type: 'limited', revenue: 8000000 },
  ],
};

query(data, 'version');
// '2.0'

query(data, 'companies.name | id == "company2"');
// 'Tech Solutions Ltd'

query(data, 'companies.name | id == "company1" \\ toUpper()');
// 'ACME CORPORATION'

queryAsArray(data, 'companies');
// [{ id: 'company1', ... }, { id: 'company2', ... }]

evaluate(data, 'status == "active"');
// true

const ctx = new QueryContext();
queryBatch(data, ctx, '$company = "company1"', 'companies.name | id == $company');
// ['', 'Acme Corporation']

Query Syntax

property.path | filter_condition \ transform()

Each part is optional after the property path:

  • Property path: navigate the JSON structure with .
  • Filter: narrow array items with |
  • Transform: modify the result with \

When writing queries in JavaScript or TypeScript strings, escape the transform delimiter as \\.

Property Access

version
companies.name
metadata.settings.enableLogging

Property access resolves nested values across both objects and arrays.

Core rule:

  • query() always returns a single resolved value.
  • If navigation reaches multiple candidates, query() returns the first matched result.
  • This is true whether the match comes from plain property navigation or from a filter.
  • Use queryAsArray() when you want every match instead of the first one.

Examples based on the test suite:

query(data, 'version');
// '2.0'

query(data, 'metadata.settings.enableLogging');
// true

query(data, 'companies.name');
// 'Acme Corporation'    // first company name

query(data, 'companies.name | id == "company2"');
// 'Tech Solutions Ltd'  // first company that matches the filter

query(data, 'companies.locations.city | locations.$index == 1');
// 'Los Angeles'         // first city produced by the matched path

queryAsArray(data, 'companies');
// returns all companies

Filters

Use filters after | to select matching array items.

Comparison Operators

Operator Example
== companies.name | id == "company1"
!= companies.name | type != "limited"
> < >= <= companies.name | revenue > 10000000
in companies.name | id in ["company1", "company3"]
not in companies.name | id not in ["company2"]
like companies.name | name like "%Corp%"
not like companies.name | name not like "%Ltd"
contains companies.name | name contains "Solutions"
startswith companies.name | name startswith "Tech"
endswith companies.name | name endswith "Ltd"
is null companies.name | description is null
is not null companies.name | description is not null
between companies.name | revenue between 8000000 and 16000000

Combine Conditions

Note: && and || inside a filter expression are not currently parsed as separate conditions. The operator and everything after it becomes part of the right-hand value, so the compound filter will never match. Use two separate query() calls and combine the results in JavaScript instead.

// ✗ Does not work — && is not parsed as a logical operator in filters
query(data, 'employees.name | department == "Engineering" && isRemote == false');

// ✓ Workaround — two queries, one JS condition
const name   = query(data, 'employees.name     | department == "Engineering"');
const remote = query(data, 'employees.isRemote | department == "Engineering"');
if (!remote) { /* John Doe */ }

Filter by Position with $index

The library injects $index while iterating arrays.

companies.name | $index == 1
companies.locations.city | locations.$index == 1

Cross-Reference Another Query

Use parentheses to resolve another query and compare against its result.

companies.name | id == (employees.companyId | id == "emp1")

Variables and Aliases

For reusable queries, create a QueryContext and pass it to query() or queryBatch().

Variables

Assign a value once and reference it later with a $ prefix.

import { QueryContext, query } from '@nanotiny/json-expression';

const ctx = new QueryContext();

query(data, '$companyId = "company1"', ctx);
query(data, 'companies.name | id == $companyId', ctx);
// 'Acme Corporation'

Notes:

  • Assignments return an empty string because they update the context instead of selecting data.
  • Variables work well with queryBatch() when several queries need shared state.

Aliases

Aliases shorten long paths and make complex expressions easier to read.

import { QueryContext, query } from '@nanotiny/json-expression';

const ctx = new QueryContext();

query(data, 'companies as comp | comp.id == "company2"', ctx);
query(data, 'comp.name', ctx);
// 'Tech Solutions Ltd'

Use aliases when the same root path appears repeatedly in filters or transforms.

Transforms

Apply transforms after \. In code, write them as \\ inside strings.

String Transforms

Transform Example Result
toUpper() name \ toUpper() "ACME"
toLower() name \ toLower() "acme"
substring(start, length) name \ substring(0, 4) "Acme"
split(delimiter) name \ split(" ") ["Acme", "Corporation"]
replace(old, new) name \ replace("Corp", "Co") "Acme Co"
trim() desc \ trim() trimmed string
padLeft(width, char) id \ padLeft(12, "0") "0000company1"
padRight(width, char) id \ padRight(12, ".") "company1...."

Numeric Transforms

Transform Example Result
round(decimals) price \ round(1) 3128431.2
ceiling() price \ ceiling() 3128432
floor() price \ floor() 3128431
abs() value \ abs() absolute value
toNumFormat(format) version \ toNumFormat("00.0") "02.0"

Array Transforms

Transform Example Description
count() companies \ count() Number of elements
min() values \ min() Minimum numeric value
max() values \ max() Maximum numeric value
sum() values \ sum() Sum of numeric values
average() values \ average() Average of numeric values
slice(start, length) companies \ slice(0, 2) Sub-array
sort(prop, dir) companies \ sort("name", "asc") Sorted copy

Conversion and Conditional Transforms

Transform Example Result
toRoman() count \ toRoman() "III"
numToText() count \ numToText() "Three"
toDateFormat(fmt) createdAt \ toDateFormat(yyyy-MM-dd) "2024-04-16"
if(cond, true, false) price \ if("> 3000000", "expensive", "affordable") "expensive"
ifBlank(default) desc \ ifBlank("N/A") "N/A" if blank

Chain Transforms

query(data, 'companies.name | id == "company1" \\ toUpper() \\ substring(0, 4)');
// 'ACME'

query(data, 'companies \\ count() \\ toRoman()');
// 'II'

API Reference

query(data, queryStr, context?)

Returns the first matching value.

query(data, 'companies.name | id == "company1"');
// 'Acme Corporation'

Use context when you need variables or aliases to persist across calls.

queryAsArray(data, queryStr)

Returns every matching value as an array.

queryAsArray(data, 'companies');
// [{ id: 'company1', ... }, { id: 'company2', ... }]

evaluate(data, condition)

Evaluates a condition against the input data and returns a boolean.

evaluate(data, 'version == "2.0"');
// true

evaluate(data, 'status == "inactive"');
// false

Note: The same &&/|| parser limitation applies here. Use two separate evaluate() calls and combine with JavaScript && / ||.

evaluate(data, 'version == "2.0"') && evaluate(data, 'status == "active"');
// true

queryBatch(data, ...queries)

Executes multiple queries and returns the results in order.

queryBatch(data, 'version', 'status', 'companies.name | id == "company1"');
// ['2.0', 'active', 'Acme Corporation']

queryBatch(data, context, ...queries)

Executes multiple queries with a shared QueryContext.

const ctx = new QueryContext();

queryBatch(
  data,
  ctx,
  '$company = "company2"',
  'companies.name | id == $company',
);
// ['', 'Tech Solutions Ltd']

QueryContext

Stores variables and aliases across multiple queries.

const ctx = new QueryContext();

query(data, '$id = "company1"', ctx);
query(data, 'companies.name | id == $id', ctx);
// 'Acme Corporation'

Edge Cases

Path Not Found

When a key does not exist anywhere in the path, query() returns undefined. Use the ifBlank() transform or the ?? operator to supply a default.

Scenario Returns
Root key missing (query(data, 'noSuchKey')) undefined
Nested key missing (query(data, 'companies.noSuchField')) undefined
Filter with no matching item undefined
Key exists, value is null or blank '' (empty string)
query(data, 'nonExistentKey');                         // undefined
query(data, 'companies.nonExistentField');             // undefined
query(data, 'companies.name | id == "ghost"');         // undefined

query(data, 'companies.description | id == "co2"');    // '' (null in data)
query(data, 'companies.description | id == "co2" \\ ifBlank("N/A")'); // 'N/A'

// Safe default in JavaScript
const value = (query(data, 'someKey') ?? 'default') as string;

Invalid Filter Syntax

A filter expression that uses an unrecognised operator or is structurally malformed throws an Error at parse time.

try {
  query(data, 'companies.name | id ?? "company1"');  // unknown operator ??
} catch (e) {
  // Error: Invalid filter expression: id ?? "company1"
}

try {
  query(data, 'companies.name | id');  // missing operator and right operand
} catch (e) {
  // Error: Invalid filter expression: id
}

Always validate user-supplied query strings with a try/catch when the input comes from external sources.

Type Mismatch in Comparisons

All operand values are coerced to strings before comparison. There is no runtime type error — mismatched types produce a silent non-match instead.

Operator Behaviour on type mismatch
== / != String equality after coercion — 42 becomes "42", true becomes "true"
> < >= <= Uses parseFloat — a non-numeric value becomes NaN and the comparison returns false
between Uses parseFloat on both bounds — non-numeric silently returns false
contains / startswith / endswith / like Applied to the stringified value — always safe
// Numeric field vs unquoted number → both stringify to "120000" → matches
query(data, 'employees.name | salary == 120000');   // 'John Doe'

// Numeric field vs quoted string → quotes stripped, string-equal → still matches
query(data, 'employees.name | salary == "120000"'); // 'John Doe'

// Ordering: non-numeric value on numeric operator → NaN → no match, no error
query(data, 'employees.name | name > 100');         // undefined (silent)

// Boolean field: stored as boolean, coerced to "true"/"false" for comparison
query(data, 'companies.name | isActive == true');   // 'Acme Corporation'
query(data, 'companies.name | isActive == "true"'); // 'Acme Corporation' (quotes stripped)

Compatibility

  • Node.js 14 or later
  • Modern browsers with ES2020 support
  • ESM and CommonJS builds
  • TypeScript declarations included

License

MIT