From 82ddf6de2cb5ca5d1f1045db0c997cb6a9e8dfd0 Mon Sep 17 00:00:00 2001 From: Seles Date: Tue, 7 Jul 2026 17:49:28 +0200 Subject: [PATCH] Fix ClassCastException on write operations with non-matching paths and JsonNode-based providers JsonContext write operations (set/delete/map/renameKey) evaluate the path with Option.AS_PATH_LIST and cast the result to List. In the missing-path branch (SUPPRESS_EXCEPTIONS), handleMissingPathInContext returned jsonProvider.createArray() - a document-model object that is not a java.util.List for JsonNode-based providers (Jackson's ArrayNode) - causing a ClassCastException. Return an empty java.util.List instead, matching the type returned by EvaluationContextImpl.getPathList() in the nominal case. --- .../main/java/com/jayway/jsonpath/JsonPath.java | 9 ++++++++- .../Jackson3JsonNodeJsonProviderTest.java | 17 +++++++++++++++++ .../JacksonJsonNodeJsonProviderTest.java | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java index 01c7db03a..8c4308f25 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.ArrayList; import static com.jayway.jsonpath.Option.ALWAYS_RETURN_LIST; import static com.jayway.jsonpath.Option.AS_PATH_LIST; @@ -749,7 +750,13 @@ private T handleMissingPathInContext(final Configuration configuration) { boolean optAsPathList = configuration.containsOption(AS_PATH_LIST); boolean optAlwaysReturnList = configuration.containsOption(Option.ALWAYS_RETURN_LIST); if (optAsPathList) { - return (T) configuration.jsonProvider().createArray(); + // AS_PATH_LIST results are java.util.List path lists (see + // resultByConfiguration / EvaluationContextImpl.getPathList), consumed by the + // write operations of JsonContext which cast them to List. Returning + // jsonProvider.createArray() here broke that contract with document-model + // providers whose arrays are not java.util.List (e.g. Jackson's ArrayNode), + // throwing ClassCastException on non-matching paths with SUPPRESS_EXCEPTIONS. + return (T) new ArrayList(); } else { if (optAlwaysReturnList) { return (T) configuration.jsonProvider().createArray(); diff --git a/json-path/src/test/java/com/jayway/jsonpath/Jackson3JsonNodeJsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/Jackson3JsonNodeJsonProviderTest.java index 06b595a54..27c9a28d4 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/Jackson3JsonNodeJsonProviderTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/Jackson3JsonNodeJsonProviderTest.java @@ -281,4 +281,21 @@ public static final class Data { } } + + // Write operations on a non-matching path used to throw ClassCastException with + // SUPPRESS_EXCEPTIONS: handleMissingPathInContext returned jsonProvider.createArray() + // (an ArrayNode with this provider) where JsonContext casts the AS_PATH_LIST result + // to java.util.List. Root cause of zalando/logbook#1369. + @Test + public void set_on_non_matching_path_with_suppressed_exceptions_does_not_throw() { + Configuration conf = JACKSON3_JSON_NODE_CONFIGURATION.addOptions(Option.SUPPRESS_EXCEPTIONS); + DocumentContext context = using(conf).parse(JSON_DOCUMENT); + + context.set("$..does-not-exist.value", "XXX"); + context.delete("$..does-not-exist"); + context.renameKey("$..does-not-exist", "old", "new"); + + assertThat(context.read("$.string-property", String.class)).isEqualTo("string-value"); + } + } diff --git a/json-path/src/test/java/com/jayway/jsonpath/JacksonJsonNodeJsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/JacksonJsonNodeJsonProviderTest.java index 1684248b2..056a464e4 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/JacksonJsonNodeJsonProviderTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/JacksonJsonNodeJsonProviderTest.java @@ -279,4 +279,21 @@ public static final class Data { } } + + // Write operations on a non-matching path used to throw ClassCastException with + // SUPPRESS_EXCEPTIONS: handleMissingPathInContext returned jsonProvider.createArray() + // (an ArrayNode with this provider) where JsonContext casts the AS_PATH_LIST result + // to java.util.List. Root cause of zalando/logbook#1369. + @Test + public void set_on_non_matching_path_with_suppressed_exceptions_does_not_throw() { + Configuration conf = JACKSON_JSON_NODE_CONFIGURATION.addOptions(Option.SUPPRESS_EXCEPTIONS); + DocumentContext context = using(conf).parse(JSON_DOCUMENT); + + context.set("$..does-not-exist.value", "XXX"); + context.delete("$..does-not-exist"); + context.renameKey("$..does-not-exist", "old", "new"); + + assertThat(context.read("$.string-property", String.class)).isEqualTo("string-value"); + } + }