From 28bacb1a2520279ca8ac757d7fd008632d204615 Mon Sep 17 00:00:00 2001 From: simonkundrik <117163790+simonkundrik@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:06:18 +0000 Subject: [PATCH] fix: look up quoted names with brackets, semicolons or quotes as written Quoted names containing a bracket, a semicolon, a caret, an apostrophe (inside double quotes), a leading double quote or at sign, or a hash followed by a digit were not looked up as written. toPathArray splits the expression at those characters before the name is read, so $['cd[DataLayer]'], $["it's"], $['x;y'] and $['@id'] found nothing or threw, and the result paths for such properties could not be fed back into a query. A single quoted name with one of those characters is now kept intact in toPathArray, with a leading backtick (the existing escape) so that it is looked up as it is. Other names take the same route as before. Fixes #167, fixes #203, fixes #225 and fixes #258. Also covers the quoted forms in #173. --- src/jsonpath.js | 16 ++++++++++++++ test/test.properties.js | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/jsonpath.js b/src/jsonpath.js index 4d036ee..2eed181 100644 --- a/src/jsonpath.js +++ b/src/jsonpath.js @@ -1251,6 +1251,22 @@ JSONPath.toPathArray = function (expr) { return '[#' + (subx.push($1) - 1) + ']'; } ) + // Quoted property names containing characters which would otherwise + // be read as path syntax (e.g., `['a[b]']`), kept intact with a + // leading backtick escape so they are looked up as they are + .replaceAll( + /\[\s*(?:'([^']*)'|"([^"]*)")\s*\]/gv, + function ($0, single, double) { + const name = single ?? double; + if (!(/(?:^[@"])|[\[\];^']|#\d/v).test(name)) { + return $0; + } + const escaped = name[0] === '`' ? name : '`' + name; + // eslint-disable-next-line @stylistic/max-len -- Long + // eslint-disable-next-line unicorn/no-return-array-push -- Optimization + return '[#' + (subx.push(escaped) - 1) + ']'; + } + ) // Escape periods and tildes within properties .replaceAll(/\[['"]([^'\]]*)['"]\]/gv, function ($0, prop) { return "['" + prop diff --git a/test/test.properties.js b/test/test.properties.js index 66febf1..03fb2b9 100644 --- a/test/test.properties.js +++ b/test/test.properties.js @@ -91,6 +91,55 @@ checkBuiltInVMAndNodeVM(function (vmType, setBuiltInState) { } }); + it('Quoted property names with path syntax characters', () => { + const names = [ + 'cd[DataLayer]', '[', ']', 'phoneNumbers [1]', 'x;y', 'a^b', + "it's", "a'", "'lastName'", '"lastName"', 'say "hi"', '@id' + ]; + for (const name of names) { + const data = {[name]: {y: 'Y'}, x: {[name]: 'N'}, z: 1}; + /** + * @param {string|string[]} path + * @param {"value"|"path"} [resultType] + * @param {boolean} [wrap] + * @returns {unknown} + */ + const query = (path, resultType, wrap = true) => jsonpath({ + json: data, path, resultType, wrap + }); + const quotedNames = /** @type {string[]} */ ([]); + if (!name.includes("'")) { + quotedNames.push(`'${name}'`); + } + if (!name.includes('"')) { + quotedNames.push(`"${name}"`); + } + for (const quoted of quotedNames) { + const msg = quoted; + assert.deepEqual(query(`$[${quoted}]`), [{y: 'Y'}], msg); + assert.deepEqual(query(`$[${quoted}].y`), ['Y'], msg); + assert.deepEqual(query(`$.x[${quoted}]`), ['N'], msg); + assert.deepEqual( + query(`$..[${quoted}]`), [{y: 'Y'}, 'N'], msg + ); + // A single name is not an array expression + assert.strictEqual( + query(`$[${quoted}].y`, 'value', false), 'Y', msg + ); + } + // Result paths and array paths find the name again + const [path] = /** @type {string[]} */ ( + query('$.*.y', 'path') + ); + assert.deepEqual(query(path), ['Y'], path); + assert.deepEqual(query(['$', name, 'y']), ['Y'], name); + } + // A leading backtick still escapes the rest of the name + assert.deepEqual(jsonpath({ + json: {'a[b]': 1, '`a[b]': 2}, path: "$['`a[b]']" + }), [1]); + }); + it('At signs within properties', () => { let result = jsonpath({json, path: "$.datafield[?(@.tag=='035')]", wrap: false}); assert.deepEqual(result, [json.datafield[0]]);