npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nanotiny/json-expression

v0.0.13

Published

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

Downloads

1,312

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.

Try the live demo →

Install

npm install @nanotiny/json-expression

Highlights

  • Zero runtime dependencies
  • Works in Node.js and modern browsers
  • Supports filtering, transforms, and boolean evaluation
  • Extensible with custom transform functions
  • 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 | type == "corporation" && revenue > 10000000');
// 'Acme Corporation'

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 | | && \|\| | companies.name \| type == "corporation" && isActive == "true" |

like and not like use SQL-style wildcard matching: % matches any sequence, _ matches a single character, and other characters are treated literally.

Combine Conditions

Use && (AND) or || (OR) to combine multiple conditions. && has higher precedence than ||, so conditions are evaluated as OR-separated groups of AND expressions.

// AND — both conditions must match
query(data, 'companies.name | type == "corporation" && isActive == "true"');
// 'Acme Corporation'

// OR — either condition can match (first matching item is returned)
query(data, 'companies.name | type == "limited" || type == "corporation"');
// 'Acme Corporation'

// Mix AND and OR (`&&` binds tighter than `||`)
query(data, 'companies.name | type == "corporation" || isActive == "true" && revenue < 1000');
// 'Acme Corporation'

Explicit Boolean Grouping with []

Use [ and ] when you want to force a particular boolean grouping.

query(data, 'companies.name | [type == "limited" || type == "corporation"] && isActive == "false"');
// 'Global Innovations Corp'

query(data, 'companies.name | [id == (employees.companyId | id == "emp1") || id == "company2"]');
// 'Acme Corporation'

evaluate(data, '[version == "1.0" || status == "active"] && metadata.settings.maxRetries == 5');
// true

Notes:

  • Quoted string values can include literal && or || (for example name == "foo && bar"). Those sequences are only treated as boolean operators when they appear outside quoted values.
  • Using multiple | delimiters in one query (e.g. path | cond1 | cond2) throws an Error at parse time. Use && or || inside a single filter instead.
  • Parentheses () are reserved for cross-reference sub-queries, not boolean grouping.
  • Square brackets [] are reserved for explicit boolean grouping.
  • || groups must stay within a single array level in query() filters. For example, id == "company1" || type == "limited" is fine, but [id == "company1" || locations.$index == 1] throws because it mixes company-level and location-level conditions inside one OR group.

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'

Custom Transforms

You can register your own transform functions to extend the built-in set. Custom transforms work with the same \transformName(args) syntax.

Global Registration

Use registerTransform() to make a custom transform available in all queries.

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

// Register a custom transform
registerTransform('reverse', (value) => {
  return String(value ?? '').split('').reverse().join('');
});

query(data, 'status\\reverse()');
// 'evitca'

// Remove it when no longer needed
unregisterTransform('reverse');

Context-Scoped Registration

Register transforms on a QueryContext for per-query or scoped usage.

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

const ctx = new QueryContext();
ctx.registerTransform('double', (value) => {
  return String(value).repeat(2);
});

query(data, 'status\\double()', ctx);
// 'activeactive'

Custom Transform Function Signature

type CustomTransformFn = (
  value: unknown,          // current pipeline value (single element)
  parameters: string[],    // parsed parameters from the expression
  originalArray: unknown,  // the raw input before array-to-single extraction
) => unknown;

The originalArray parameter gives access to the full array for aggregate transforms:

registerTransform('joinwith', (value, parameters, originalArray) => {
  const separator = parameters[0] ?? ', ';
  const source = Array.isArray(originalArray) ? originalArray : [value];
  return source.join(separator);
});

query(data, 'metadata.tags\\joinwith(-)');
// 'production-enterprise-verified'

Override Built-in Transforms

Custom transforms take priority over built-in ones with the same name. Context-level transforms take priority over global ones.

// Override built-in toupper globally
registerTransform('toupper', (value) => `CUSTOM:${String(value).toUpperCase()}`);

query(data, 'status\\toupper()');
// 'CUSTOM:ACTIVE'

// Unregister to restore built-in behavior
unregisterTransform('toupper');

query(data, 'status\\toupper()');
// 'ACTIVE'

Chain Custom and Built-in Transforms

Custom transforms chain seamlessly with built-in ones.

registerTransform('exclaim', (value) => `${value}!`);

query(data, 'status\\toupper()\\exclaim()');
// 'ACTIVE!'

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

registerTransform(name, fn)

Registers a custom transform function globally.

registerTransform('reverse', (value, parameters, originalArray) => {
  return String(value ?? '').split('').reverse().join('');
});

unregisterTransform(name)

Removes a globally registered custom transform, restoring the built-in if one exists.

unregisterTransform('reverse');

QueryContext

Stores variables, aliases, and custom transforms across multiple queries.

const ctx = new QueryContext();

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

// Register a scoped custom transform
ctx.registerTransform('double', (value) => String(value).repeat(2));
query(data, 'status\\double()', ctx);
// 'activeactive'

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, is structurally malformed, or contains a stray | 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
}

try {
  // Multiple | delimiters — use && or || inside a single filter instead
  query(data, 'companies.name | type == "corporation" | isActive == "true"');
} catch (e) {
  // Error: Invalid filter expression: "..." contains a stray "|".
  //        Use "||" for logical OR, or split into separate query() calls.
}

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 |

For like, matching is SQL-style rather than regex-based: % matches any sequence, _ matches a single character, and characters such as ., (, ) and [ keep their literal meaning.

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