This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch jp
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 9b5107f182f0111f96fb0ec5c68791701b3c37f1
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Mar 10 19:39:08 2026 +0100

    CAMEL-23170: camel-core - Add simple jsonpath to Camels JsonObject
---
 .../org/apache/camel/util/json/JsonObject.java     | 176 ++++++++++++++++++++-
 .../apache/camel/util/json/JsonObjectPathTest.java | 136 ++++++++++++++++
 2 files changed, 311 insertions(+), 1 deletion(-)

diff --git 
a/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java
 
b/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java
index e3c93873a2be..100ee9b68316 100644
--- 
a/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java
+++ 
b/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java
@@ -24,6 +24,7 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * JsonObject is a common non-thread safe data format for string to data 
mappings. The contents of a JsonObject are only
@@ -49,12 +50,185 @@ public class JsonObject extends LinkedHashMap<String, 
Object> implements Jsonabl
      * Instantiate a new JsonObject by accepting a map's entries, which could 
lead to de/serialization issues of the
      * resulting JsonObject since the entry values aren't validated as JSON 
values.
      *
+     * The json-path syntax also supports arrays using square brackets with 
the position index, such as
+     * "foo.bar[0].title", "foo.bar[1].title". You can use "last" as index 
number to get the last element of the array.
+     *
      * @param map represents the mappings to produce the JsonObject with.
      */
     public JsonObject(final Map<String, ?> map) {
         super(map);
     }
 
+    /**
+     * A very basic json-path that can retrieve leaf node using a dot syntax, 
such as "foo.bar.title", to get the title
+     * attribute from the leaf JsonObject object (ie bar).
+     *
+     * The json-path syntax also supports arrays using square brackets with 
the position index, such as
+     * "foo.bar[0].title", "foo.bar[1].title". You can use "last" as index 
number to get the last element of the array.
+     *
+     * The returned value is expected to be a String
+     *
+     * @throws IllegalArgumentException if the path traversal does not exist
+     */
+    public String pathString(final String path) {
+        boolean optional = path.startsWith("?");
+        JsonObject jo = this;
+        String sub = path;
+        if (path.contains(".")) {
+            // grab until last dot
+            int pos = path.lastIndexOf(".");
+            optional |= '?' == path.charAt(pos - 1);
+            sub = path.substring(pos + 1);
+            if (optional) {
+                pos = pos - 1;
+            }
+            java.util.Optional<JsonObject> o = doPath(path.substring(0, pos));
+            if (o.isPresent()) {
+                jo = o.get();
+            } else {
+                optional = true;
+                jo = null;
+            }
+        }
+        String answer = jo != null ? jo.getString(sub) : null;
+        if (answer == null && !optional) {
+            throw new IllegalArgumentException("JSonObject path " + path + " 
is null");
+        }
+        return answer;
+    }
+
+    /**
+     * A very basic json-path that can retrieve leaf node using a dot syntax, 
such as "foo.bar.title", to get the title
+     * attribute from the leaf JsonObject object (ie bar).
+     *
+     * The json-path syntax also supports arrays using square brackets with 
the position index, such as
+     * "foo.bar[0].title", "foo.bar[1].title". You can use "last" as index 
number to get the last element of the array.
+     *
+     * You can use ?. to mark a path as optional, which returns null instead 
of throwing an exception if the path
+     * traversal does not exist.
+     *
+     * The returned value is expected to be an Integer
+     *
+     * @throws IllegalArgumentException if the path traversal does not exist
+     */
+    public Integer pathInteger(String path) {
+        boolean optional = path.startsWith("?");
+        JsonObject jo = this;
+        String sub = path;
+        if (path.contains(".")) {
+            // grab until last dot
+            int pos = path.lastIndexOf(".");
+            optional |= '?' == path.charAt(pos - 1);
+            sub = path.substring(pos + 1);
+            if (optional) {
+                pos = pos - 1;
+            }
+            java.util.Optional<JsonObject> o = doPath(path.substring(0, pos));
+            if (o.isPresent()) {
+                jo = o.get();
+            } else {
+                optional = true;
+                jo = null;
+            }
+        }
+        Integer answer = jo != null ? jo.getInteger(sub) : null;
+        if (answer == null && !optional) {
+            throw new IllegalArgumentException("JSonObject path " + path + " 
is null");
+        }
+        return answer;
+    }
+
+    /**
+     * A very basic json-path that can retrieve leaf node using a dot syntax, 
such as "foo.bar.title", to get the title
+     * attribute from the leaf JsonObject object (ie bar).
+     *
+     * The returned value is expected to be a Boolean
+     *
+     * @throws IllegalArgumentException if the path traversal does not exist
+     */
+    public Boolean pathBoolean(String path) {
+        boolean optional = path.startsWith("?");
+        JsonObject jo = this;
+        String sub = path;
+        if (path.contains(".")) {
+            // grab until last dot
+            int pos = path.lastIndexOf(".");
+            optional |= '?' == path.charAt(pos - 1);
+            sub = path.substring(pos + 1);
+            if (optional) {
+                pos = pos - 1;
+            }
+            java.util.Optional<JsonObject> o = doPath(path.substring(0, pos));
+            if (o.isPresent()) {
+                jo = o.get();
+            } else {
+                optional = true;
+                jo = null;
+            }
+        }
+        Boolean answer = jo != null ? jo.getBoolean(sub) : null;
+        if (answer == null && !optional) {
+            throw new IllegalArgumentException("JSonObject path " + path + " 
is null");
+        }
+        return answer;
+    }
+
+    /**
+     * A very basic json-path that can retrieve a JSonObject node using a dot 
syntax, such as "foo.bar", to get the bar
+     * JSonObject.
+     */
+    public JsonObject path(final String path) {
+        return doPath(path).orElse(null);
+    }
+
+    private Optional<JsonObject> doPath(final String path) {
+        Jsonable answer = null;
+
+        String[] split = path.splitWithDelimiters("(\\?\\.|\\.)", 0);
+        for (int i = 0; i < split.length; i = i + 2) {
+            String part = split[i];
+            String dot = i > 0 ? split[i - 1] : null;
+            int pos = -1;
+            boolean optional;
+            if (part.startsWith("?")) {
+                part = part.substring(1);
+                optional = true;
+            } else {
+                optional = dot != null && dot.equals("?.");
+            }
+            // notice multi-level index is not supported, such as[0][0] or 
[0][1]
+            if (part.endsWith("]")) {
+                String num = part.substring(part.lastIndexOf('[') + 1, 
part.length() - 1);
+                if ("last".equals(num)) {
+                    pos = Integer.MAX_VALUE;
+                } else {
+                    pos = Integer.parseInt(num);
+                }
+                part = part.substring(0, part.lastIndexOf('['));
+            }
+            if (answer instanceof JsonObject jo) {
+                answer = pos == -1 ? jo.getMap(part) : jo.getJsonArray(part);
+            } else {
+                answer = pos == -1 ? getMap(part) : getJsonArray(part);
+            }
+            if (pos != -1 && answer instanceof JsonArray arr) {
+                if (pos == Integer.MAX_VALUE) {
+                    answer = (Jsonable) arr.getLast();
+                } else if (pos < arr.size()) {
+                    answer = (Jsonable) arr.get(pos);
+                } else {
+                    answer = null;
+                }
+            }
+            if (answer == null && !optional) {
+                throw new IllegalArgumentException("JSonObject path " + path + 
" at " + part + " does not exist");
+            } else if (answer == null) {
+                return Optional.empty();
+            }
+        }
+        return Optional.of(JsonObject.class.cast(answer));
+    }
+
     /**
      * A convenience method that assumes there is another JsonObject at the 
given key.
      *
@@ -796,4 +970,4 @@ public class JsonObject extends LinkedHashMap<String, 
Object> implements Jsonabl
         }
         writable.write('}');
     }
-}
+};
diff --git 
a/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java
 
b/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java
new file mode 100644
index 000000000000..5f0fc66b2f51
--- /dev/null
+++ 
b/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.util.json;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class JsonObjectPathTest {
+
+    private static final String BOOKS
+            = """
+                    {
+                        "library": {
+                            "book": [
+                                {
+                                    "title": "No Title",
+                                    "author": "F. Scott Fitzgerald",
+                                    "year": "1925",
+                                    "genre": "Classic",
+                                    "movie": false,
+                                    "id": "bk101"
+                                },
+                                {
+                                    "title": "1984",
+                                    "author": "George Orwell",
+                                    "year": "1949",
+                                    "genre": "Dystopian",
+                                    "movie": true,
+                                    "id": "bk102"
+                                }
+                            ]
+                        }
+                    }
+                    """;
+
+    @Test
+    public void testPath() throws Exception {
+        JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS);
+
+        JsonObject obj = jo.path("library.book[0]");
+        Assertions.assertNotNull(obj);
+        Assertions.assertEquals("No Title", obj.getString("title"));
+
+        obj = jo.path("library.book[1]");
+        Assertions.assertNotNull(obj);
+        Assertions.assertEquals("1984", obj.getString("title"));
+    }
+
+    @Test
+    public void testPathAttribute() throws Exception {
+        JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS);
+        Assertions.assertNotNull(jo);
+
+        Assertions.assertEquals("No Title", 
jo.pathString("library.book[0].title"));
+        Assertions.assertEquals(1925, jo.pathInteger("library.book[0].year"));
+        Assertions.assertFalse(jo.pathBoolean("library.book[0].movie"));
+        Assertions.assertEquals("1984", 
jo.pathString("library.book[1].title"));
+        Assertions.assertEquals(1949, jo.pathInteger("library.book[1].year"));
+        Assertions.assertTrue(jo.pathBoolean("library.book[1].movie"));
+
+        try {
+            Assertions.assertNull(jo.path("library.book[1].unknown"));
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testPathOptional() throws Exception {
+        JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS);
+        Assertions.assertNotNull(jo);
+
+        Assertions.assertNotNull(jo.path("library?.book[0]"));
+        Assertions.assertNull(jo.path("library?.book[2]"));
+        try {
+            Assertions.assertNull(jo.path("library.book[2]"));
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testPathAttributeOptional() throws Exception {
+        JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS);
+        Assertions.assertNotNull(jo);
+
+        Assertions.assertNull(jo.pathString("library?.book[2]?.subTitle"));
+        Assertions.assertEquals("No Title", 
jo.pathString("library.book[0].title"));
+        Assertions.assertNull(jo.pathString("library.book[0]?.subTitle"));
+        Assertions.assertNull(jo.pathString("?unknown?.book[0].title"));
+
+        Assertions.assertNull(jo.pathString("library?.book[2].title"));
+        Assertions.assertNull(jo.pathInteger("library?.book[2].year"));
+        Assertions.assertNull(jo.pathBoolean("library?.book[2].movie"));
+    }
+
+    @Test
+    public void testPathAttributeArrayLast() throws Exception {
+        JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS);
+        Assertions.assertNotNull(jo);
+
+        Assertions.assertEquals("No Title", 
jo.pathString("library.book[0].title"));
+        Assertions.assertEquals(1925, jo.pathInteger("library.book[0].year"));
+        Assertions.assertEquals("1984", 
jo.pathString("library.book[last].title"));
+        Assertions.assertEquals(1949, 
jo.pathInteger("library.book[last].year"));
+    }
+
+    @Test
+    public void testPathAttributeLeafNode() throws Exception {
+        JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS);
+        Assertions.assertNotNull(jo);
+
+        jo = (JsonObject) 
jo.getJsonObject("library").getJsonArray("book").get(0);
+        Assertions.assertEquals("No Title", jo.pathString("title"));
+        Assertions.assertEquals(1925, jo.pathInteger("year"));
+    }
+
+}

Reply via email to