From 4a93e34c79976e67bcd4f942d954c8349b26acec Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:42:16 +0700 Subject: [PATCH] fix: accept single-quoted string args in path functions (#1078) Recognize single-quoted string literals as JSON function parameters and track the active quote delimiter so mixed quotes group correctly. Normalize single-quoted literals to double-quoted JSON for strict providers (e.g. Jackson). Fixes #1078 --- .../jsonpath/internal/path/PathCompiler.java | 53 +++++++++++++++---- .../internal/function/NestedFunctionTest.java | 14 +++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java index 27b6e0633..aba5c1134 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java @@ -290,7 +290,10 @@ private List parseFunctionParameters(String funcName) { // Parenthesis starts at 1 since we're marking the start of a function call, the close paren will denote the // last parameter boundary - int groupParen = 1, groupBracket = 0, groupBrace = 0, groupQuote = 0; + // quote tracks the active string delimiter (0 when not inside a quoted string) so single- and double-quoted + // literals can both be used as JSON parameters without treating the other quote as a terminator. + int groupParen = 1, groupBracket = 0, groupBrace = 0; + char quote = 0; boolean endOfStream = false; char priorChar = 0; List parameters = new ArrayList(); @@ -305,7 +308,7 @@ private List parseFunctionParameters(String funcName) { continue; } - if (c == OPEN_BRACE || isDigit(c) || DOUBLE_QUOTE == c || MINUS == c) { + if (c == OPEN_BRACE || isDigit(c) || DOUBLE_QUOTE == c || SINGLE_QUOTE == c || MINUS == c) { type = ParamType.JSON; } else if (isPathContext(c)) { @@ -315,11 +318,13 @@ else if (isPathContext(c)) { switch (c) { case DOUBLE_QUOTE: - if (priorChar != '\\' && groupQuote > 0) { - groupQuote--; - } - else { - groupQuote++; + case SINGLE_QUOTE: + if (priorChar != '\\') { + if (quote == 0) { + quote = c; + } else if (quote == c) { + quote = 0; + } } break; case OPEN_PARENTHESIS: @@ -356,7 +361,7 @@ else if (isPathContext(c)) { case COMMA: // In this state we've reach the end of a function parameter and we can pass along the parameter string // to the parser - if ((0 == groupQuote && 0 == groupBrace && 0 == groupBracket + if ((0 == quote && 0 == groupBrace && 0 == groupBracket && ((0 == groupParen && CLOSE_PARENTHESIS == c) || 1 == groupParen))) { endOfStream = (0 == groupParen); @@ -364,8 +369,10 @@ else if (isPathContext(c)) { Parameter param = null; switch (type) { case JSON: - // parse the json and set the value - param = new Parameter(parameter.toString()); + // parse the json and set the value. + // Single-quoted string literals are accepted in path function args + // (same as filter predicates) but are not valid JSON; normalize them. + param = new Parameter(normalizeFunctionParameterJson(parameter.toString())); break; case PATH: LinkedList predicates = new LinkedList<>(); @@ -398,6 +405,32 @@ private boolean isWhitespace(char c) { return (c == SPACE || c == TAB || c == LF || c == CR); } + /** + * Convert a path-language single-quoted string literal into a JSON double-quoted string so + * providers that require strict JSON (e.g. Jackson) can parse the function parameter. + * Non-string JSON tokens (numbers, objects, arrays, double-quoted strings) are left unchanged. + */ + private static String normalizeFunctionParameterJson(String json) { + if (json == null) { + return null; + } + int start = 0; + int end = json.length(); + while (start < end && Character.isWhitespace(json.charAt(start))) { + start++; + } + while (end > start && Character.isWhitespace(json.charAt(end - 1))) { + end--; + } + if (end - start >= 2 + && json.charAt(start) == SINGLE_QUOTE + && json.charAt(end - 1) == SINGLE_QUOTE) { + String unescaped = Utils.unescape(json.substring(start + 1, end - 1)); + return DOUBLE_QUOTE + Utils.escape(unescaped, false) + DOUBLE_QUOTE; + } + return json; + } + // // [?], [?,?, ..] // diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java index 36faf6563..4fc03b175 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java @@ -86,6 +86,20 @@ public void testStringConcatWithJSONParameter(Configuration conf) { verifyTextFunction(conf, "$.text.concat(\"-\", \"ghijk\")", "abcdef-ghijk"); } + /** + * Single-quoted string literals in function args should match double-quoted behavior + * (same as filter predicates). Regression for https://github.com/json-path/JsonPath/issues/1078 + */ + @ParameterizedTest + @MethodSource("configurations") + public void testStringConcatWithSingleQuotedJSONParameter(Configuration conf) { + verifyFunction(conf, "$.concat('hello')", "{}", "hello"); + verifyTextFunction(conf, "$.text.concat('-', 'ghijk')", "abcdef-ghijk"); + verifyFunction(conf, "$.concat($.x,'-',$.y)", "{\"x\":\"P\",\"y\":\"Q\"}", "P-Q"); + // Apostrophe inside double quotes must not break argument grouping + verifyFunction(conf, "$.concat(\"it's\")", "{}", "it's"); + } + @ParameterizedTest @MethodSource("configurations")