Quick answer: Run four JSONPath migration checks before changing engines: compare filter meaning, verify selected locations, preserve result shape, and define ordering and duplicate behavior. Pin the engine settings and use small fixtures with explicit expectations. Successful parsing does not establish that a query still extracts the data your application intended.
A dependency update passes, but an alert stops matching records whose flag is false. Another query still returns the expected value—from the wrong branch. These are extraction regressions, not JSON syntax errors.
The JSONPath Query Tester helps inspect sanitized documents and match counts. It runs jsonpath-plus, not a universal evaluator. For syntax background, use our JSONPath versus JSON Pointer guide and Wikipedia's JSONPath overview.
There is a concrete reason to test migrations now: Huginn's upgrade notes document a switch toward RFC 9535 behavior introduced on September 21, 2026. They warn about filter conversions, existence tests, array selection, and method calls. That is evidence for this project's migration work, not a claim that every JSONPath user needs an upgrade.
The four independent JavaScript examples below were executed with jsonpath-plus 10.4.0, the installed repository dependency. They demonstrate local assertions and boundaries, not cross-engine certification. Run equivalent fixtures in both your old and proposed engines before approving a change.
1. Do filters preserve the distinction between presence and value?
Test filter behavior with absent properties, null, false, zero, empty strings, and ordinary values. A query that checks presence is not interchangeable with a truthiness test or an explicit comparison. Record what each expression means in the configured engine, then verify that the replacement preserves the business condition rather than just accepting similar syntax.
Start with the question the application actually asks. An alert checking whether a flag was supplied differs from an alert checking whether that flag is true. A numeric threshold also differs from accepting a string that happens to contain digits. Writing the intended condition in plain English makes the expected matches reviewable before anyone edits a query.
RFC 9535 defines an existence test as selecting at least one node, regardless of that node's value. Its treatment of null does not make null equivalent to a missing node. Do not assume that a legacy evaluator's JavaScript-style condition implements those semantics.
import assert from 'node:assert/strict';
import { JSONPath } from 'jsonpath-plus';
const json = { rows: [
{ id: 'null', flag: null }, { id: 'false', flag: false },
{ id: 'zero', flag: 0 }, { id: 'empty', flag: '' },
{ id: 'missing' }, { id: 'true', flag: true }
] };
const ids = path => JSONPath({ path, json, wrap: true });
assert.deepEqual(ids('$.rows[?(@.flag)].id'), ['true']);
assert.deepEqual(ids('$.rows[?(@.flag === null)].id'), ['null']);
assert.deepEqual(ids('$.rows[?(@.flag === false)].id'), ['false']);
These assertions intentionally describe the tested jsonpath-plus behavior. The strict comparison operator shown here belongs to its JavaScript-style expression support; this is not presented as portable RFC query syntax. A standards-oriented replacement needs its own documented expression and expected output for the same business rule.
Create a fixture row for every state that changes the decision. Include numeric and string versions of a threshold value if upstream data can contain both. Our null versus missing guide explains why preserving these states matters beyond query evaluation.
During review, distinguish deliberate behavior changes from accidental ones. If the application now intends to include present false values, approve that requirement explicitly instead of calling the changed result an equivalent migration. Keep both the old result and the new expected result in the migration record, with a short explanation of the difference.
Finally, test unsupported functions and extension syntax separately. A query that throws should not be silently replaced by an empty list in the test harness. That hides a configuration failure as a legitimate no-match outcome and makes the filter look correct precisely when it never ran.
2. Does the query still select the intended source locations?
Verify where matches come from, not only what values they contain. Include literal keys with punctuation, similarly named nested properties, and unrelated branches carrying equal values. Prefer a precise path when the source structure is known, and inspect source locations when recursive descent or broad wildcards could select more than the consumer intends.
Equal values make weak evidence. Suppose the primary account and an archived account both currently have status active. A query that accidentally switches branches can produce the same string and pass a value-only test. Give the branches distinct sentinel values in a second fixture so the error becomes visible.
Literal keys deserve similar attention. A key named app.name is not the same structure as an app object containing a name property. This distinction becomes especially useful when working with flattened JSON and dotted keys. Keep both shapes in a test document rather than testing the convenient one alone.
import assert from 'node:assert/strict';
import { JSONPath } from 'jsonpath-plus';
const json = {
config: { 'app.name': 'literal', app: { name: 'nested' } },
primary: { id: 'same' }, archive: { id: 'same' }
};
assert.deepEqual(JSONPath({ path: "$['config']['app.name']", json }),
['literal']);
assert.deepEqual(JSONPath({ path: '$.config.app.name', json }), ['nested']);
assert.deepEqual(JSONPath({ path: '$.primary.id', json,
resultType: 'pointer', wrap: true }), ['/primary/id']);
assert.deepEqual(JSONPath({ path: '$..id', json,
resultType: 'pointer', wrap: true }).sort(), ['/archive/id', '/primary/id']);
The jsonpath-plus API documentation documents resultType and pointer output. Those are library API options, not a promise that every engine exposes the same interface. Adapt each engine's location reporting into one harness representation before comparing results.
The public tester displays matched values and a count; it does not list the source pointers returned in this programmatic example. Use the code API or your deployment's diagnostics when the location is part of the acceptance criterion. A tree path inside the displayed result is not automatically a source-document pointer.
For queries that feed updates or redaction, location checks are particularly valuable. Selecting the wrong equal-valued field can change a different record even though extraction output appears unchanged. Keep selection tests read-only; verify any mutation operation with its own authorization and update tests.
Do not broaden an expression merely to make a failing migration test pass. Replacing a specific child path with recursive descent can mask a renamed envelope while adding unwanted nested matches. Correct the documented structure, update the expected location deliberately, and retain a negative fixture proving that unrelated branches remain excluded.
3. Does the consumer receive the same result shape and cardinality?
Check zero, one, and multiple matches, including a match whose value is itself an array. Keep the list of matches distinct from the matched value, and verify null separately from absence. A migration can preserve individual values while changing wrapping, flattening, or no-match conventions enough to break downstream application code.
The consuming code needs a contract, not a guess based on the first sample. If an alert expects exactly one account identifier, assert exactly one match before reading the first entry. If a report accepts several records, assert the item structure and preserve the outer list even when only one record matches.
In jsonpath-plus, wrapping is configurable. The maintainer's README describes the wrap option and the behavior of unwrapped results. The repository's query helper explicitly uses wrap: true. Reproduce your real wrapper settings in migration tests instead of relying on whichever default a sandbox happens to use.
import assert from 'node:assert/strict';
import { JSONPath } from 'jsonpath-plus';
const json = { users: [{ tags: ['red', 'blue'], note: null }] };
const query = path => JSONPath({ path, json, wrap: true });
assert.deepEqual(query('$.users[9].tags'), []);
assert.deepEqual(query('$.users[0].note'), [null]);
assert.deepEqual(query('$.users[0].tags'), [['red', 'blue']]);
assert.deepEqual(query('$.users[0].tags[*]'), ['red', 'blue']);
assert.deepEqual(query('$.users[*]'), [json.users[0]]);
This fixture distinguishes an array-valued single match from two scalar matches. Automatically flattening both to the same list would erase that distinction. Compare types and nesting depth before choosing an adapter for the destination library. Our JSON array versus object guide covers the application-facing consequences of changing container shape.
Also record the failure channel. A no-match list, a syntax error, and an evaluator failure must remain distinguishable. Catching every exception and returning an empty array creates false confidence because negative fixtures can pass without exercising the query successfully. Let unexpected errors fail the migration test visibly.
Use sanitized production-shaped fixtures as well as tiny examples. Include optional envelopes, empty collections, and records missing the target property. These cases reveal assumptions in the integration wrapper that a library-only example cannot exercise. Check the final serialized result delivered to the caller, not just an intermediate runtime object.
Before release, have the consumer owner approve the cardinality rule. Taking the first match is safe only when the contract permits choosing one or guarantees uniqueness through separate validation. A query engine returning a list does not establish that your business identifier is unique, and a migration should not silently invent that guarantee.
4. Are ordering and duplicate rules explicit in the comparison?
Decide whether consumers require an ordered sequence, repeated matches, or unique source locations before comparing engines. Test slices and repeated selectors explicitly. Do not sort or deduplicate every result to make differences disappear; normalization must reflect the consumer's contract and preserve enough diagnostic information to explain a changed selection or count.
Consider a query used to build a ranked list. Changing order changes the feature. A query used only to verify that certain source locations exist may allow location-order differences instead. Those two consumers need different assertions, even when they happen to use the same expression today.
The JSONPath compliance suite explicitly accommodates cases with more than one valid result ordering. Use the suite when evaluating standards behavior, and add your application fixtures alongside it. Passing four examples here is not a compliance result, and passing a compliance suite does not establish your application's business rules.
import assert from 'node:assert/strict';
import { JSONPath } from 'jsonpath-plus';
const json = { items: ['A', 'B', 'C'] };
const query = path => JSONPath({ path, json, wrap: true });
assert.deepEqual(query('$.items[0,0]'), ['A', 'A']);
assert.deepEqual(query('$.items[-2:]'), ['B', 'C']);
assert.deepEqual(query('$.items[0:2]'), ['A', 'B']);
const multiplicities = values => {
const counts = new Map();
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
return [...counts].sort(([a], [b]) => a.localeCompare(b));
};
assert.deepEqual(multiplicities(['A', 'B', 'A']), [['A', 2], ['B', 1]]);
assert.notDeepEqual(multiplicities(['A', 'A']), multiplicities(['A']));
The counting helper is intentionally limited to these string fixtures. It is not a general JSON equality or canonicalization algorithm. For objects, compare stable source identities or use a deliberately specified structural comparison. Avoid serializing arbitrary objects into keys and assuming that property ordering defines semantic equality.
Recent developer reports reinforce the need for boundary fixtures: Jayway issue 1076, opened July 21, 2026, reports a negative slice returning duplicates. That report concerns a particular implementation and reproduction, not a claim about all engines or the tested jsonpath-plus version. Include the slice boundaries your application actually uses and verify them directly.
Keep migration evidence reproducible: record engine versions, configuration, expressions, fixture files, raw outputs, and approved expectations. Run the same corpus before and after a dependency change. When outputs differ, classify the difference before rewriting the query: syntax rejection, changed selection, changed wrapping, or ordering policy.
Release only after each unexplained difference has an owner and a decision. A temporary compatibility mode can buy review time when a platform supports one, but it does not remove the need for tests. Keep rollback settings with the previous engine configuration, and rerun the fixtures after deployment to confirm the integrated application still uses the evaluator and options you approved.