You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/03/10 18:39:30 UTC

(camel) branch jp created (now 9b5107f182f0)

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

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


      at 9b5107f182f0 CAMEL-23170: camel-core - Add simple jsonpath to Camels JsonObject

This branch includes the following new commits:

     new 9b5107f182f0 CAMEL-23170: camel-core - Add simple jsonpath to Camels JsonObject

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-23170: camel-core - Add simple jsonpath to Camels JsonObject

Posted by da...@apache.org.
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 <cl...@gmail.com>
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"));
+    }
+
+}