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 🙏

© 2025 – Pkg Stats / Ryan Hefner

json-remap-engine

v0.3.0

Published

JSONPath-driven remapping engine that produces JSON Patch operations.

Downloads

62

Readme

json-remap-engine

npm version npm downloads license

A lightweight, fully client-side rules engine that rewrites JSON documents by evaluating JSONPath matchers and producing JSON Patch operations. The core logic is extracted from Token Tamer and packaged for reuse in build scripts, CLIs, and browser applications.

  • 💡 JSONPath matchers decide which values to prune, replace, or move.
  • 🩺 Rich diagnostics list every rule, match, and warning so you can render UI feedback or fail builds.
  • 🛠️ JSON Patch output (remove, replace, move) lets you persist the changes or run them through existing patch tools.
  • 🧾 TOON text output (optional) – render the transformed JSON as compact human-readable text using @byjohann/toon with sensible defaults.
  • 📦 Framework agnostic – ships as ESM and CJS bundles with TypeScript declarations.

Installation

npm install json-remap-engine

The package targets Node 18+ and modern browsers (requires structuredClone or falls back to JSON serialization).

Quick start

import {
  runTransformer,
  createRemoveRule,
  createReplaceRule,
  createMoveRule,
  encodeToToon,
  OutputEncoding,
  type EncodeOptions,
} from "json-remap-engine";

const rules = [
  createRemoveRule("$.payload.largeBlob"),
  createReplaceRule("$.status", "published"),
  createReplaceRule("$.title", "$.metadata.safeTitle"),
  createReplaceRule("$.problematic_tests[*].styles[?(@.sriError==false)]", "$.styles.safe"),
  createRemoveRule("$.problematic_tests[?(@.inspection && @.inspection.meta && @.inspection.meta.status == 'OK')].inspection"),
  createMoveRule("$.draft.body", "$.published[0].body"),
];

const { document, operations, diagnostics, ok } = runTransformer(input, rules);

if (!ok) {
  console.error(diagnostics.flatMap((rule) => rule.errors));
}

console.log(document);   // transformed JSON
console.log(operations); // JSON Patch operations that were applied

// Optional: encoded output via the same function
const encoded = runTransformer(input, rules, { encoding: OutputEncoding.Toon });
console.log(encoded.output); // TOON text (tab-delimited, '#' length marker by default)

Example transformation

This sample applies every rule type, including an in-place rename that derives its new key from the matched object.

Source JSON

{
  "summary": {
    "status": "draft",
    "services": [
      {
        "service": { "id": "svc-001", "description": "" },
        "metadata": { "alias": "service_now" },
        "analytics": { "hits": 0 }
      }
    ]
  },
  "legacyTitle": "Legacy release name"
}

Rules (assuming the helper factories are imported from json-remap-engine)

const source = /* JSON from the previous block */;

const rules = [
  createReplaceRule("$.summary.status", "published", { id: "publish-status" }),
  createRenameRule("$.summary.services[*].service", "$.metadata.alias", {
    id: "rename-service",
    targetMode: "jsonpath",
  }),
  createRemoveRule("$.summary.services[*].analytics", { id: "drop-analytics" }),
  createMoveRule("$.legacyTitle", "/summary/title", { id: "hoist-title" }),
];

const { document, operations } = runTransformer(source, rules);

Transformed document

{
  "summary": {
    "status": "published",
    "services": [
      {
        "service_now": { "id": "svc-001", "description": "" },
        "metadata": { "alias": "service_now" }
      }
    ],
    "title": "Legacy release name"
  }
}

Patch operations

[
  { "op": "replace", "path": "/summary/status", "value": "published" },
  { "op": "move", "from": "/summary/services/0/service", "path": "/summary/services/0/service_now" },
  { "op": "remove", "path": "/summary/services/0/analytics" },
  { "op": "move", "from": "/legacyTitle", "path": "/summary/title" }
]

The rename rule runs against each service object. The JSONPath $.metadata.alias is resolved relative to the matched object, so array indices never leak into the expression, and the emitted patch still uses the standard move operation.

Why another JSON remapping approach?

  • There is no single, standards-track "XSLT for JSON." Specs such as JSONPath, JSON Pointer, and JSON Patch solve slices of the problem, but teams still resort to bespoke remapping glue.
  • JSONPath excels at discovering nodes (filters, wildcards, script expressions) yet stops short of templating or producing concrete mutations. Authors have to wrap selectors with imperative code.
  • JSON Patch (RFC 6902) and JSON Pointer (RFC 6901) provide the minimal, auditable set of operations—but hand-writing large pointer-based rule sets for documents with evolving shapes is brittle.

json-remap-engine bridges these gaps: rules describe discovery in JSONPath while the engine emits standards-compliant JSON Patch operations with normalized pointers for downstream tooling and audits.

Alternatives

If you need template-driven transformations or full expression languages, consider:

  • JSLT – JSONPath-inspired selectors paired with a functional templating language.
  • JSONata – declarative queries with mapping, aggregation, and user-defined functions.
  • Jolt – a Java DSL aimed at streaming pipeline remaps.

These tools are powerful but ship larger interpreters and opinionated runtimes. json-remap-engine stays lightweight for build steps, CLIs, and browser diagnostics that need deterministic JSONPath-to-JSON Patch bridging.

Rule builders

The helper factories mirror the original UI defaults and add a few ergonomics for library consumers. All helpers accept an optional options object with id, allowEmptyMatcher, allowEmptyValue, and disabled flags. When using move rules, allowEmptyValue: true now treats unresolved targets as a no-op instead of producing an error.

| Helper | Purpose | Key defaults | | --- | --- | --- | | createRemoveRule(matcher, options) | Removes every JSONPath match | allowEmptyMatcher=false | | createReplaceRule(matcher, value, options) | Replaces each match with a literal value or another JSONPath value | valueMode="auto" detects JSONPath when strings start with $; pass valueMode: "literal" to keep strings like "$100" | | createMoveRule(matcher, target, options) | Moves the source match to the target (JSON Pointer by default) | targetMode="auto" interprets leading / as JSON Pointer, leading $ as JSONPath | | createRenameRule(matcher, target, options) | Renames object property keys in place | targetMode="auto" treats $/@ prefixes as parent-scoped JSONPath; literal strings are trimmed and applied directly |

For full control you can construct Rule objects manually.

Move rules are hardened against prototype-pollution attacks: targets containing __proto__, constructor, or prototype segments are rejected. Additionally, when allowEmptyValue is true they quietly skip execution if the target JSONPath resolves to zero pointers.

import type { Rule } from "json-remap-engine";

const customRule: Rule = {
  id: "warn",
  matcher: "$.items[?(@.status == 'deprecated')]",
  op: "remove",
  allowEmptyMatcher: true,
};

Diagnostics & error handling

runTransformer returns a TransformerResult:

interface TransformerResult {
  ok: boolean;              // true when no rule reported errors
  document: unknown;        // cloned & transformed input
  operations: JsonPatchOperation[]; // applied operations in execution order
  diagnostics: RuleDiagnostic[];    // per-rule detail (matches, errors, warnings)
  errors: string[];         // flattened list of rule errors
  warnings: string[];       // flattened list of rule warnings
}

When a rule fails its matcher or target it remains in diagnostics with status: "skipped" and a human-friendly message so applications can bubble the failure to users.

Pointer utilities

Additional helpers are exported for converting between analysis paths, JSONPath, and JSON Pointer strings:

  • analysisPathToPointer("root.payload.items[0]") // => "/payload/items/0"
  • simpleJsonPathToPointer("$.payload.items[0]") // => "/payload/items/0"
  • pointerExists(document, "/payload/items/0")
  • simpleJsonPathToPointer("$.problematic_tests[*].styles[?(@.sriError==false)]") // => null (wildcards with filters are intentionally unsupported)
  • simpleJsonPathToPointer("$.problematic_tests[?(@.inspection && @.inspection.meta && @.inspection.meta.status == 'OK')].inspection") // => null (requires guarded access to avoid runtime errors)

These utilities are reused internally when resolving move targets but exposed for downstream tooling.

Encoded output (JSON or TOON)

The library can optionally return a TOON representation of the transformed document.

  • encodeToToon(value, options?) – encode any JSON-compatible value.
  • runTransformer(input, rules, { encoding }) – parameterize output encoding while keeping the same API. Use OutputEncoding.JsonPretty, OutputEncoding.JsonCompact, or OutputEncoding.Toon.

Defaults (EncodeOptions) favor compact readability:

import { defaultToonOptions, OutputEncodingDescription, type EncodeOptions } from "json-remap-engine";
// defaultToonOptions = { delimiter: "\t", indent: 2, lengthMarker: "#" }

You can override options and the types are re-exported for convenience:

const options: EncodeOptions = { delimiter: "|", indent: 2 };
const { output } = runTransformer(input, rules, { encoding: OutputEncoding.Toon, toonOptions: options });

// Use descriptions for UI selections
console.log(OutputEncodingDescription.Toon); // "TOON text format using @byjohann/toon."

JSON Schema

A machine-readable definition of the rule format lives at docs/rules.schema.json. The file targets JSON Schema Draft 2020-12 and advertises its $id as https://json-remap-engine.dev/schemas/rules.schema.json so external tooling can $ref it directly.

Known limitations & compatibility notes

  • Replacement strings that start with $ are treated as JSONPath expressions by default. Use valueMode: "literal" when you need the literal $ prefix.
  • Move targets resolved via JSONPath must map to exactly one pointer; ambiguous matches raise errors.
  • Complex JSONPath constructs (filters, script expressions) are evaluated by jsonpath-plus. Only “simple” paths (dot, bracket, numeric indices) can be converted to pointers when the JSONPath returns zero matches.
  • Deep cloning falls back to JSON.parse(JSON.stringify(...)), so values such as BigInt or Map will not survive cloning.
  • Pointer segments named __proto__, constructor, or prototype are rejected for mutating operations to guard against prototype pollution. Pointer utilities also avoid treating inherited properties as existing members.

JSON Patch compliance

The engine emits only remove, replace, and move operations and applies them using the pointer semantics from RFC 6902 and RFC 6901. Rename rules lower to JSON Patch move operations so downstream tooling remains fully compliant:

  • remove requires the pointer to resolve and executes array deletions in descending index order to preserve RFC removal guarantees.
  • replace requires the pointer to resolve before overwriting, mirroring the RFC requirement to test existence first.
  • move internally resolves from, removes it, and re-inserts the cloned value at the destination using the same constraints as RFC add.

Because operations are applied to a cloned working document, the resulting JSON Patch array can be replayed with any compliant RFC 6902 implementation.

Scripts

  • npm run build – bundle to dist/ (CJS, ESM, and declaration files)
  • npm run test – run the Vitest suite
  • npm run lint – TypeScript type-check (no emit)
  • npm run check – type-check then tests

Continuous integration

GitHub Actions runs the same checks on every push and pull request via .github/workflows/ci.yml. The workflow uses npm ci, executes npm run check, and builds the production bundles on Node.js 18.x and 20.x.

Published GitHub releases automatically trigger .github/workflows/release.yml, which repeats the checks and runs npm publish --access public. Store an npm automation token in the repository secret NPM_TOKEN before tagging a release.

License

MIT