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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new bf7518dd0b Issue apache#2362 Evaluate JsonPath functions in the JSON 
Input transform (#8198)
bf7518dd0b is described below

commit bf7518dd0b249053d13ee66f68d202d0ee746d59
Author: Sergio Ramazzina <[email protected]>
AuthorDate: Fri Sep 25 11:21:30 2026 +0200

    Issue apache#2362 Evaluate JsonPath functions in the JSON Input transform 
(#8198)
    
    * Issue apache#2362 Evaluate JsonPath functions in the JSON Input transform
    
    * Issue apache#2362 Honour SUPPRESS_EXCEPTIONS for failing JsonPath 
functions
    
    * Issue apache#2362 Add integration test for JsonPath functions in JSON 
Input
    
    * minor cleanup and extra tests
    
    ---------
    
    Co-authored-by: Hans Van Akelyen <[email protected]>
---
 .../ROOT/pages/pipeline/transforms/jsoninput.adoc  |  14 +
 .../json/0015-json-input-jsonpath-functions.hpl    | 210 +++++++++++++++
 .../0016-json-input-jsonpath-functions-rows.hpl    | 293 +++++++++++++++++++++
 .../0017-json-input-jsonpath-functions-missing.hpl | 207 +++++++++++++++
 .../golden-json-input-jsonpath-functions-lines.csv |   4 +
 ...olden-json-input-jsonpath-functions-missing.csv |   4 +
 ...golden-json-input-jsonpath-functions-totals.csv |   2 +
 .../golden-json-input-jsonpath-functions.csv       |   4 +
 .../main-0015-json-input-jsonpath-functions.hwf    |  80 ++++++
 ...ain-0016-json-input-jsonpath-functions-rows.hwf |  80 ++++++
 ...-0017-json-input-jsonpath-functions-missing.hwf |  80 ++++++
 ...golden-json-input-jsonpath-functions-lines.json |  25 ++
 ...lden-json-input-jsonpath-functions-missing.json |  41 +++
 ...olden-json-input-jsonpath-functions-totals.json |  33 +++
 .../golden-json-input-jsonpath-functions.json      |  41 +++
 .../0015-json-input-jsonpath-functions UNIT.json   |  43 +++
 ...16-json-input-jsonpath-functions-rows UNIT.json |  56 ++++
 ...json-input-jsonpath-functions-missing UNIT.json |  43 +++
 .../jsoninput/reader/FastJsonReader.java           |  85 +++++-
 .../jsoninput/messages/messages_en_US.properties   |   1 +
 .../jsoninput/reader/FastJsonReaderTest.java       | 219 +++++++++++++++
 21 files changed, 1560 insertions(+), 5 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc
index ab51eb67f3..59a4c9ab44 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc
@@ -169,6 +169,20 @@ You can use Regex in the Path expression.
 
 *Example Path to filter on those that start with the letter 'a':* 
$.data[?(@=~/a.*/i)]
 
+*JsonPath functions*
+
+The aggregation functions provided by JayWay can be used at the end of a Path 
expression, e.g. `length()` (or its alias `size()`), `sum()`, `avg()`, `min()`, 
`max()`, `stddev()`, `keys()`, `first()`, `last()` and `index()`.
+
+*Example Path to retrieve the number of elements of an array:* 
$.someArray.length()
+
+A function is applied to the value the Path points to. When the Path points to 
a single value, the function produces a single value and therefore a single 
output row, even when that value is itself a list, as with `keys()`.
+
+When the Path contains a wildcard before the function, the function is applied 
to every match and produces one row per match. For example, 
`$.orders[*].lines.length()` returns the number of lines of each order. To 
aggregate over all matches instead, use a deep scan or pass the Path as a 
parameter to the function: `$..price.sum()` or `$.sum($.orders[*].price)`. Note 
that `$.orders[*].price.sum()` does not work: the sum is applied to each single 
price.
+
+Combining a single-value function field with a field whose Path matches 
several elements is not supported: the transform reports a structure error 
because the two Paths do not yield the same number of rows. Read such a value 
in a dedicated JSON Input transform when the other fields return multiple rows.
+
+When a function cannot be evaluated, for example `sum()` or `first()` on an 
empty array, the value is treated as missing: it is empty when *Ignore missing 
path* is enabled, otherwise the transform fails with the reason.
+
 
 === Additional output fields tab
 
diff --git a/integration-tests/json/0015-json-input-jsonpath-functions.hpl 
b/integration-tests/json/0015-json-input-jsonpath-functions.hpl
new file mode 100644
index 0000000000..e87504dc6f
--- /dev/null
+++ b/integration-tests/json/0015-json-input-jsonpath-functions.hpl
@@ -0,0 +1,210 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+  <info>
+    <name>0015-json-input-jsonpath-functions</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Verifies that JsonPath functions (length(), sum(), max()) are 
evaluated by the JSON Input transform</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/01 09:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/01 09:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Dummy JSON</from>
+      <to>Get Attributes</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Get Attributes</from>
+      <to>Validate</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Dummy JSON</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>{"name": "Goku", "moves": [{"moveName": "Kamehameha"}, 
{"moveName": "Spirit Bomb"}], "powers": [9500, 12000]}</item>
+      </line>
+      <line>
+        <item>{"name": "Vegeta", "moves": [{"moveName": "Galick Gun"}], 
"powers": [9000]}</item>
+      </line>
+      <line>
+        <item>{"name": "Gohan", "moves": [{"moveName": "Masenko"}, 
{"moveName": "Father-Son Kamehameha"}, {"moveName": "Ki Blast"}], "powers": 
[8500, 11000, 3000]}</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>z_fighter</name>
+        <type>JSON</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>192</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Get Attributes</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>Y</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>N</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>name</name>
+        <path>$.name</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>moveCount</name>
+        <path>$.moves.length()</path>
+        <type>Integer</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>totalPower</name>
+        <path>$.powers.sum()</path>
+        <type>Number</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>maxPower</name>
+        <path>$.powers.max()</path>
+        <type>Number</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>z_fighter</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>352</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Validate</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/json/0016-json-input-jsonpath-functions-rows.hpl 
b/integration-tests/json/0016-json-input-jsonpath-functions-rows.hpl
new file mode 100644
index 0000000000..1eab101c96
--- /dev/null
+++ b/integration-tests/json/0016-json-input-jsonpath-functions-rows.hpl
@@ -0,0 +1,293 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+  <info>
+    <name>0016-json-input-jsonpath-functions-rows</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Verifies JsonPath functions behind a wildcard (one row per 
match), aggregation across matches and a list-valued function</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/25 09:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/25 09:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Orders</from>
+      <to>Order lines</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Order lines</from>
+      <to>Validate lines</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Orders</from>
+      <to>Order totals</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Order totals</from>
+      <to>Validate totals</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Orders</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>{"customer": "Acme", "orders": [{"id": "A1", "lines": [1, 2], 
"price": 10}, {"id": "A2", "lines": [1, 2, 3], "price": 20}, {"id": "A3", 
"lines": [], "price": 5}]}</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>doc</name>
+        <type>String</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>128</xloc>
+      <yloc>176</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Order lines</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>Y</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>N</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>orderId</name>
+        <path>$.orders[*].id</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>lineCount</name>
+        <path>$.orders[*].lines.length()</path>
+        <type>Integer</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>doc</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>112</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Validate lines</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>112</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Order totals</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>Y</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>N</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>totalPrice</name>
+        <path>$..price.sum()</path>
+        <type>Number</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>maxPrice</name>
+        <path>$.max($.orders[*].price)</path>
+        <type>Number</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>orderKeys</name>
+        <path>$.orders[0].keys()</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>doc</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Validate totals</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git 
a/integration-tests/json/0017-json-input-jsonpath-functions-missing.hpl 
b/integration-tests/json/0017-json-input-jsonpath-functions-missing.hpl
new file mode 100644
index 0000000000..295f85b1dc
--- /dev/null
+++ b/integration-tests/json/0017-json-input-jsonpath-functions-missing.hpl
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+  <info>
+    <name>0017-json-input-jsonpath-functions-missing</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Verifies that a JsonPath function that cannot be evaluated 
gives an empty value when Ignore missing path is enabled</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/25 09:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/25 09:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Documents</from>
+      <to>Get Values</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Get Values</from>
+      <to>Validate</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Documents</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>{"items": [], "tags": ["x", "y"]}</item>
+      </line>
+      <line>
+        <item>{"items": [3, 4], "tags": ["z"]}</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>doc</name>
+        <type>JSON</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>192</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Get Values</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>N</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>Y</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>Y</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>tag</name>
+        <path>$.tags[*]</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>firstItem</name>
+        <path>$.items.first()</path>
+        <type>Integer</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>itemTotal</name>
+        <path>$.items.sum()</path>
+        <type>Number</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+      <field>
+        <name>absentCount</name>
+        <path>$.absent.length()</path>
+        <type>Integer</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>doc</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>352</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Validate</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git 
a/integration-tests/json/datasets/golden-json-input-jsonpath-functions-lines.csv
 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions-lines.csv
new file mode 100644
index 0000000000..33e366e98c
--- /dev/null
+++ 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions-lines.csv
@@ -0,0 +1,4 @@
+orderId,lineCount
+A1,2
+A2,3
+A3,0
diff --git 
a/integration-tests/json/datasets/golden-json-input-jsonpath-functions-missing.csv
 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions-missing.csv
new file mode 100644
index 0000000000..94a935d1fd
--- /dev/null
+++ 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions-missing.csv
@@ -0,0 +1,4 @@
+tag,firstItem,itemTotal,absentCount
+x,,,
+y,,,
+z,3,7.0,
diff --git 
a/integration-tests/json/datasets/golden-json-input-jsonpath-functions-totals.csv
 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions-totals.csv
new file mode 100644
index 0000000000..341086e03d
--- /dev/null
+++ 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions-totals.csv
@@ -0,0 +1,2 @@
+totalPrice,maxPrice,orderKeys
+35.0,20.0,"[id, lines, price]"
diff --git 
a/integration-tests/json/datasets/golden-json-input-jsonpath-functions.csv 
b/integration-tests/json/datasets/golden-json-input-jsonpath-functions.csv
new file mode 100644
index 0000000000..01b57f2762
--- /dev/null
+++ b/integration-tests/json/datasets/golden-json-input-jsonpath-functions.csv
@@ -0,0 +1,4 @@
+name,moveCount,totalPower,maxPower
+Goku,2,21500.0,12000.0
+Vegeta,1,9000.0,9000.0
+Gohan,3,22500.0,11000.0
diff --git a/integration-tests/json/main-0015-json-input-jsonpath-functions.hwf 
b/integration-tests/json/main-0015-json-input-jsonpath-functions.hwf
new file mode 100644
index 0000000000..ebfcedfe0d
--- /dev/null
+++ b/integration-tests/json/main-0015-json-input-jsonpath-functions.hwf
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+  <name>main-0015-json-input-jsonpath-functions</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/01 09:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/01 09:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0015-json-input-jsonpath-functions UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/json/main-0016-json-input-jsonpath-functions-rows.hwf 
b/integration-tests/json/main-0016-json-input-jsonpath-functions-rows.hwf
new file mode 100644
index 0000000000..4b92240476
--- /dev/null
+++ b/integration-tests/json/main-0016-json-input-jsonpath-functions-rows.hwf
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+  <name>main-0016-json-input-jsonpath-functions-rows</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/25 09:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/25 09:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0016-json-input-jsonpath-functions-rows UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/json/main-0017-json-input-jsonpath-functions-missing.hwf 
b/integration-tests/json/main-0017-json-input-jsonpath-functions-missing.hwf
new file mode 100644
index 0000000000..f30233f843
--- /dev/null
+++ b/integration-tests/json/main-0017-json-input-jsonpath-functions-missing.hwf
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+  <name>main-0017-json-input-jsonpath-functions-missing</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/25 09:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/25 09:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0017-json-input-jsonpath-functions-missing UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-lines.json
 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-lines.json
new file mode 100644
index 0000000000..9db5c9caec
--- /dev/null
+++ 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-lines.json
@@ -0,0 +1,25 @@
+{
+  "base_filename": "golden-json-input-jsonpath-functions-lines.csv",
+  "virtualPath": "",
+  "name": "golden-json-input-jsonpath-functions-lines",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "orderId",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": -1,
+      "field_name": "lineCount",
+      "field_format": ""
+    }
+  ],
+  "folder_name": "${HOP_DATASETS_FOLDER}"
+}
diff --git 
a/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-missing.json
 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-missing.json
new file mode 100644
index 0000000000..653e3dff85
--- /dev/null
+++ 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-missing.json
@@ -0,0 +1,41 @@
+{
+  "base_filename": "golden-json-input-jsonpath-functions-missing.csv",
+  "virtualPath": "",
+  "name": "golden-json-input-jsonpath-functions-missing",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "tag",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": -1,
+      "field_name": "firstItem",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 1,
+      "field_precision": -1,
+      "field_name": "itemTotal",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": -1,
+      "field_name": "absentCount",
+      "field_format": ""
+    }
+  ],
+  "folder_name": "${HOP_DATASETS_FOLDER}"
+}
diff --git 
a/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-totals.json
 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-totals.json
new file mode 100644
index 0000000000..cdeca9f4fe
--- /dev/null
+++ 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions-totals.json
@@ -0,0 +1,33 @@
+{
+  "base_filename": "golden-json-input-jsonpath-functions-totals.csv",
+  "virtualPath": "",
+  "name": "golden-json-input-jsonpath-functions-totals",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 1,
+      "field_precision": -1,
+      "field_name": "totalPrice",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 1,
+      "field_precision": -1,
+      "field_name": "maxPrice",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "orderKeys",
+      "field_format": ""
+    }
+  ],
+  "folder_name": "${HOP_DATASETS_FOLDER}"
+}
diff --git 
a/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions.json
 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions.json
new file mode 100644
index 0000000000..fb86b3c8bc
--- /dev/null
+++ 
b/integration-tests/json/metadata/dataset/golden-json-input-jsonpath-functions.json
@@ -0,0 +1,41 @@
+{
+  "base_filename": "golden-json-input-jsonpath-functions.csv",
+  "virtualPath": "",
+  "name": "golden-json-input-jsonpath-functions",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "name",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": -1,
+      "field_name": "moveCount",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 1,
+      "field_precision": -1,
+      "field_name": "totalPower",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 1,
+      "field_precision": -1,
+      "field_name": "maxPower",
+      "field_format": ""
+    }
+  ],
+  "folder_name": "${HOP_DATASETS_FOLDER}"
+}
diff --git 
a/integration-tests/json/metadata/unit-test/0015-json-input-jsonpath-functions 
UNIT.json 
b/integration-tests/json/metadata/unit-test/0015-json-input-jsonpath-functions 
UNIT.json
new file mode 100644
index 0000000000..04adb8cc7d
--- /dev/null
+++ 
b/integration-tests/json/metadata/unit-test/0015-json-input-jsonpath-functions 
UNIT.json    
@@ -0,0 +1,43 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "name",
+          "data_set_field": "name"
+        },
+        {
+          "transform_field": "moveCount",
+          "data_set_field": "moveCount"
+        },
+        {
+          "transform_field": "totalPower",
+          "data_set_field": "totalPower"
+        },
+        {
+          "transform_field": "maxPower",
+          "data_set_field": "maxPower"
+        }
+      ],
+      "field_order": [
+        "name",
+        "moveCount",
+        "totalPower",
+        "maxPower"
+      ],
+      "data_set_name": "golden-json-input-jsonpath-functions",
+      "transform_name": "Validate"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0015-json-input-jsonpath-functions UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0015-json-input-jsonpath-functions.hpl"
+}
diff --git 
a/integration-tests/json/metadata/unit-test/0016-json-input-jsonpath-functions-rows
 UNIT.json 
b/integration-tests/json/metadata/unit-test/0016-json-input-jsonpath-functions-rows
 UNIT.json
new file mode 100644
index 0000000000..dd64826e7f
--- /dev/null
+++ 
b/integration-tests/json/metadata/unit-test/0016-json-input-jsonpath-functions-rows
 UNIT.json       
@@ -0,0 +1,56 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "orderId",
+          "data_set_field": "orderId"
+        },
+        {
+          "transform_field": "lineCount",
+          "data_set_field": "lineCount"
+        }
+      ],
+      "field_order": [
+        "orderId",
+        "lineCount"
+      ],
+      "data_set_name": "golden-json-input-jsonpath-functions-lines",
+      "transform_name": "Validate lines"
+    },
+    {
+      "field_mappings": [
+        {
+          "transform_field": "totalPrice",
+          "data_set_field": "totalPrice"
+        },
+        {
+          "transform_field": "maxPrice",
+          "data_set_field": "maxPrice"
+        },
+        {
+          "transform_field": "orderKeys",
+          "data_set_field": "orderKeys"
+        }
+      ],
+      "field_order": [
+        "totalPrice",
+        "maxPrice",
+        "orderKeys"
+      ],
+      "data_set_name": "golden-json-input-jsonpath-functions-totals",
+      "transform_name": "Validate totals"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0016-json-input-jsonpath-functions-rows UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0016-json-input-jsonpath-functions-rows.hpl"
+}
diff --git 
a/integration-tests/json/metadata/unit-test/0017-json-input-jsonpath-functions-missing
 UNIT.json 
b/integration-tests/json/metadata/unit-test/0017-json-input-jsonpath-functions-missing
 UNIT.json
new file mode 100644
index 0000000000..1e7720b74b
--- /dev/null
+++ 
b/integration-tests/json/metadata/unit-test/0017-json-input-jsonpath-functions-missing
 UNIT.json    
@@ -0,0 +1,43 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "tag",
+          "data_set_field": "tag"
+        },
+        {
+          "transform_field": "firstItem",
+          "data_set_field": "firstItem"
+        },
+        {
+          "transform_field": "itemTotal",
+          "data_set_field": "itemTotal"
+        },
+        {
+          "transform_field": "absentCount",
+          "data_set_field": "absentCount"
+        }
+      ],
+      "field_order": [
+        "tag",
+        "firstItem",
+        "itemTotal",
+        "absentCount"
+      ],
+      "data_set_name": "golden-json-input-jsonpath-functions-missing",
+      "transform_name": "Validate"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0017-json-input-jsonpath-functions-missing UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0017-json-input-jsonpath-functions-missing.hpl"
+}
diff --git 
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReader.java
 
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReader.java
index cc4cd5acf8..fe8e3c6539 100644
--- 
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReader.java
+++ 
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReader.java
@@ -24,11 +24,13 @@ import com.jayway.jsonpath.JsonPath;
 import com.jayway.jsonpath.Option;
 import com.jayway.jsonpath.ParseContext;
 import com.jayway.jsonpath.ReadContext;
+import com.jayway.jsonpath.internal.path.PathCompiler;
 import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
 import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
 import java.io.InputStream;
 import java.util.AbstractList;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.RandomAccess;
@@ -54,11 +56,29 @@ public class FastJsonReader implements IJsonReader {
   /** used if the incoming value is a JsonNode */
   private final Configuration jsonNodeConfiguration;
 
+  /**
+   * Jayway refuses path functions under ALWAYS_RETURN_LIST, so function paths 
are read with these
+   * copies of the configurations above that leave the option out.
+   */
+  private Configuration jsonFunctionConfiguration;
+
+  private final Configuration jsonNodeFunctionConfiguration;
+
+  /** The function configuration matching the current read context. */
+  private Configuration functionConfiguration;
+
   private boolean ignoreMissingPath;
   @Getter private boolean defaultPathLeafToNull;
 
   private JsonInputField[] fields;
   private JsonPath[] paths = null;
+
+  /** Per path: it ends in a function such as length(). */
+  private boolean[] functionPaths = new boolean[0];
+
+  /** Per path: it matches at most one element, so a function on it yields one 
value. */
+  private boolean[] definitePaths = new boolean[0];
+
   private final ILogChannel log;
 
   private static final Option[] DEFAULT_OPTIONS = {
@@ -70,6 +90,8 @@ public class FastJsonReader implements IJsonReader {
     this.defaultPathLeafToNull = true;
     this.jsonConfiguration = 
Configuration.defaultConfiguration().addOptions(DEFAULT_OPTIONS);
     this.jsonNodeConfiguration = getJacksonNodeJsonPathConfig();
+    this.jsonFunctionConfiguration = 
withoutAlwaysReturnList(this.jsonConfiguration);
+    this.jsonNodeFunctionConfiguration = 
withoutAlwaysReturnList(this.jsonNodeConfiguration);
     this.log = log;
   }
 
@@ -90,6 +112,7 @@ public class FastJsonReader implements IJsonReader {
       if (!this.defaultPathLeafToNull) {
         this.jsonConfiguration =
             deleteOptionFromConfiguration(this.jsonConfiguration, 
Option.DEFAULT_PATH_LEAF_TO_NULL);
+        this.jsonFunctionConfiguration = 
withoutAlwaysReturnList(this.jsonConfiguration);
       }
     }
   }
@@ -102,6 +125,13 @@ public class FastJsonReader implements IJsonReader {
         .build();
   }
 
+  private static Configuration withoutAlwaysReturnList(Configuration config) {
+    EnumSet<Option> options = EnumSet.noneOf(Option.class);
+    options.addAll(config.getOptions());
+    options.remove(Option.ALWAYS_RETURN_LIST);
+    return config.setOptions(options.toArray(new Option[0]));
+  }
+
   @SuppressWarnings("javabugs:S2259") // the configuration is created before 
it is logged
   private Configuration deleteOptionFromConfiguration(Configuration config, 
Option option) {
     Configuration currentConf = config;
@@ -161,6 +191,7 @@ public class FastJsonReader implements IJsonReader {
 
   protected void readInput(InputStream is) throws HopException {
     jsonReadContext = getParseContext().parse(is, Const.UTF_8);
+    functionConfiguration = jsonFunctionConfiguration;
     if (jsonReadContext == null) {
       throw new HopException(BaseMessages.getString(PKG, 
"JsonReader.Error.ReadUrl.Null"));
     }
@@ -168,6 +199,7 @@ public class FastJsonReader implements IJsonReader {
 
   protected void readInput(JsonNode node) throws HopException {
     jsonReadContext = getJsonNodeParseContext().parse(node);
+    functionConfiguration = jsonNodeFunctionConfiguration;
     if (jsonReadContext == null) {
       throw new HopException(BaseMessages.getString(PKG, 
"JsonReader.Error.ReadUrl.Null"));
     }
@@ -182,6 +214,13 @@ public class FastJsonReader implements IJsonReader {
   public void setFields(JsonInputField[] fields) throws HopException {
     this.fields = fields;
     this.paths = compilePaths(fields);
+    this.functionPaths = new boolean[paths.length];
+    this.definitePaths = new boolean[paths.length];
+    for (int i = 0; i < paths.length; i++) {
+      // JsonPath does not expose whether a path ends in a function; its 
compiler does.
+      functionPaths[i] = 
PathCompiler.compile(fields[i].getPath()).isFunctionPath();
+      definitePaths[i] = paths[i].isDefinite();
+    }
   }
 
   @Override
@@ -296,10 +335,11 @@ public class FastJsonReader implements IJsonReader {
     int lastSize = -1;
     String prevPath = null;
     List<List<?>> results = new ArrayList<>(paths.length);
-    int i = 0;
-    for (JsonPath path : paths) {
-      Object raw = getReadContext().read(path);
-      List<Object> result = normalizeJsonPathResult(raw);
+    for (int i = 0; i < paths.length; i++) {
+      List<Object> result =
+          functionPaths[i]
+              ? readFunctionPath(i)
+              : normalizeJsonPathResult(getReadContext().read(paths[i]));
       if (result.size() != lastSize && lastSize > 0 && !result.isEmpty()) {
         throw new JsonInputException(
             BaseMessages.getString(
@@ -317,11 +357,46 @@ public class FastJsonReader implements IJsonReader {
       results.add(result);
       lastSize = result.size();
       prevPath = fields[i].getPath();
-      i++;
     }
     return results;
   }
 
+  /**
+   * Evaluate a path that ends in a function. A definite path gives one value, 
even when that value
+   * is itself an array (keys(), first() of a nested array). A function behind 
a wildcard, such as
+   * {@code $.items[*].name.length()}, is applied to each match and gives one 
value per match. A
+   * missing value is an empty result, like a regular path that matches 
nothing.
+   */
+  private List<Object> readFunctionPath(int index) throws JsonInputException {
+    Object document = getReadContext().json();
+    Object value;
+    try {
+      value = paths[index].read(document, functionConfiguration);
+    } catch (RuntimeException e) {
+      // SUPPRESS_EXCEPTIONS does not cover errors raised by the function 
itself, such as sum() or
+      // first() on an empty array. Treat them as a missing value, or report 
the actual cause.
+      if (!isIgnoreMissingPath()) {
+        throw new JsonInputException(
+            BaseMessages.getString(
+                PKG, "JsonReader.Error.PathFunction", fields[index].getPath(), 
e.getMessage()),
+            e);
+      }
+      if (log.isDebug()) {
+        log.logDebug(
+            BaseMessages.getString(
+                PKG, "JsonReader.Error.PathFunction", fields[index].getPath(), 
e.getMessage()));
+      }
+      return Collections.emptyList();
+    }
+    if (value == null) {
+      return Collections.emptyList();
+    }
+    if (definitePaths[index]) {
+      return Collections.singletonList(value);
+    }
+    return normalizeJsonPathResult(value);
+  }
+
   public static boolean isAllNull(Iterable<?> list) {
     for (Object obj : list) {
       if (obj != null) {
diff --git 
a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsoninput/messages/messages_en_US.properties
 
b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsoninput/messages/messages_en_US.properties
index b99ea336a2..3fe6627064 100644
--- 
a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsoninput/messages/messages_en_US.properties
+++ 
b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsoninput/messages/messages_en_US.properties
@@ -182,6 +182,7 @@ JsonParser.JsonPath.Compile.Error=Error compiling JSONPath 
expression: {0}
 JsonReader.Debug.Configuration.Option.Delete={0} option has been deleted from 
JSON configuration.
 JsonReader.Debug.Configuration.Options=JSON configuration options: {0}.
 JsonReader.Error.CanNotFindPath=We can not find any data with path [{0}]\!
+JsonReader.Error.PathFunction=Could not evaluate the function in path [{0}]: 
{1}
 JsonReader.Error.ParsingFile=Error parsing file [{0}]\!
 JsonReader.Error.ParsingString=Error parsing string [{0}]\!
 JsonReader.Error.ReadUrl.Null=JSON parsing failed. Parser returned null.
diff --git 
a/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReaderTest.java
 
b/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReaderTest.java
index 7f44079b78..f9a749614a 100644
--- 
a/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReaderTest.java
+++ 
b/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReaderTest.java
@@ -20,18 +20,26 @@ package org.apache.hop.pipeline.transforms.jsoninput.reader;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.jayway.jsonpath.Option;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
+import org.apache.hop.core.IRowSet;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.pipeline.transforms.jsoninput.JsonInputField;
+import 
org.apache.hop.pipeline.transforms.jsoninput.exception.JsonInputException;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -110,4 +118,215 @@ class FastJsonReaderTest {
     mainList.add(l3);
     assertEquals(3, 
FastJsonReader.getMaxRowSize(Collections.singletonList(mainList)));
   }
+
+  private static final String ARRAY_JSON = 
"{\"someArray\":[{\"x\":1},{\"x\":2}]}";
+
+  private static IRowSet readString(String json, String path) throws 
HopException {
+    JsonInputField field = new JsonInputField("value");
+    field.setPath(path);
+    FastJsonReader reader =
+        new FastJsonReader(new JsonInputField[] {field}, 
mock(ILogChannel.class));
+    return reader.parseStringValue(new 
ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
+  }
+
+  @Test
+  void testLengthFunctionPathReturnsNumberOfArrayElements() throws Exception {
+    IRowSet rowSet = readString(ARRAY_JSON, "$.someArray.length()");
+    Object[] row = rowSet.getRow();
+    assertNotNull(row);
+    assertEquals(2, ((Number) row[0]).intValue());
+  }
+
+  @Test
+  void testSumFunctionPathIsEvaluated() throws Exception {
+    IRowSet rowSet = readString("{\"values\":[1,2,4]}", "$.values.sum()");
+    Object[] row = rowSet.getRow();
+    assertNotNull(row);
+    assertEquals(7, ((Number) row[0]).intValue());
+  }
+
+  @Test
+  void testFunctionPathReturnsSingleRow() throws Exception {
+    IRowSet rowSet = readString(ARRAY_JSON, "$.someArray.length()");
+    assertNotNull(rowSet.getRow());
+    assertNull(rowSet.getRow());
+  }
+
+  @Test
+  void testLengthFunctionPathOnJsonNodeInput() throws Exception {
+    JsonInputField field = new JsonInputField("value");
+    field.setPath("$.someArray.length()");
+    FastJsonReader reader =
+        new FastJsonReader(new JsonInputField[] {field}, 
mock(ILogChannel.class));
+    IRowSet rowSet = reader.parseJsonNodeValue(new 
ObjectMapper().readTree(ARRAY_JSON));
+    Object[] row = rowSet.getRow();
+    assertNotNull(row);
+    assertEquals(2, ((Number) row[0]).intValue());
+  }
+
+  @Test
+  void testRegularPathStillReturnsOneRowPerMatch() throws Exception {
+    IRowSet rowSet = readString(ARRAY_JSON, "$.someArray[*].x");
+    assertNotNull(rowSet.getRow());
+    assertNotNull(rowSet.getRow());
+    assertNull(rowSet.getRow());
+  }
+
+  @Test
+  void testFailingFunctionPathIsSuppressedAsNullValue() throws Exception {
+    JsonInputField field = new JsonInputField("value");
+    field.setPath("$.emptyList.sum()");
+    FastJsonReader reader =
+        new FastJsonReader(new JsonInputField[] {field}, 
mock(ILogChannel.class));
+    reader.setIgnoreMissingPath(true);
+    IRowSet rowSet =
+        reader.parseStringValue(
+            new 
ByteArrayInputStream("{\"emptyList\":[]}".getBytes(StandardCharsets.UTF_8)));
+    Object[] row = rowSet.getRow();
+    assertNotNull(row);
+    assertNull(row[0]);
+  }
+
+  private static final String ITEMS_JSON =
+      "{\"items\":[{\"name\":\"ab\",\"p\":1,\"tags\":[1,2]},"
+          + "{\"name\":\"abc\",\"p\":2,\"tags\":[1,2,3]},"
+          + 
"{\"name\":\"abcd\",\"p\":4,\"tags\":[]}],\"empty\":[],\"matrix\":[[1,2],[3]],"
+          + "\"obj\":{\"a\":1,\"b\":2}}";
+
+  private static FastJsonReader reader(boolean ignoreMissingPath, String... 
paths)
+      throws HopException {
+    JsonInputField[] fields = new JsonInputField[paths.length];
+    for (int i = 0; i < paths.length; i++) {
+      fields[i] = new JsonInputField("value" + i);
+      fields[i].setPath(paths[i]);
+    }
+    FastJsonReader reader = new FastJsonReader(fields, 
mock(ILogChannel.class));
+    reader.setIgnoreMissingPath(ignoreMissingPath);
+    return reader;
+  }
+
+  private static List<Object[]> rows(IRowSet rowSet) {
+    List<Object[]> rows = new ArrayList<>();
+    Object[] row;
+    while ((row = rowSet.getRow()) != null) {
+      rows.add(row);
+    }
+    return rows;
+  }
+
+  private static List<Object[]> readRows(String json, boolean 
ignoreMissingPath, String... paths)
+      throws HopException {
+    return rows(
+        reader(ignoreMissingPath, paths)
+            .parseStringValue(new 
ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8))));
+  }
+
+  private static List<Object[]> readNodeRows(
+      String json, boolean ignoreMissingPath, String... paths) throws 
Exception {
+    return rows(
+        reader(ignoreMissingPath, paths).parseJsonNodeValue(new 
ObjectMapper().readTree(json)));
+  }
+
+  @Test
+  void testFunctionBehindWildcardGivesOneValuePerMatch() throws Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, false, 
"$.items[*].tags.length()");
+    assertEquals(3, rows.size());
+    assertEquals(2, ((Number) rows.get(0)[0]).intValue());
+    assertEquals(3, ((Number) rows.get(1)[0]).intValue());
+    assertEquals(0, ((Number) rows.get(2)[0]).intValue());
+  }
+
+  @Test
+  void testFunctionBehindWildcardOnJsonNodeInput() throws Exception {
+    List<Object[]> rows = readNodeRows(ITEMS_JSON, false, 
"$.items[*].tags.length()");
+    assertEquals(3, rows.size());
+    assertEquals(0, ((JsonNode) rows.get(2)[0]).intValue());
+  }
+
+  @Test
+  void testFunctionBehindWildcardCombinesWithRegularField() throws Exception {
+    List<Object[]> rows =
+        readRows(ITEMS_JSON, false, "$.items[*].name", 
"$.items[*].tags.length()");
+    assertEquals(3, rows.size());
+    assertEquals("abc", rows.get(1)[0]);
+    assertEquals(3, ((Number) rows.get(1)[1]).intValue());
+  }
+
+  @Test
+  void testAggregationOverDeepScan() throws Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, false, "$..p.sum()");
+    assertEquals(1, rows.size());
+    assertEquals(7, ((Number) rows.get(0)[0]).intValue());
+  }
+
+  @Test
+  void testAggregationWithPathParameter() throws Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, false, "$.max($.items[*].p)");
+    assertEquals(1, rows.size());
+    assertEquals(4, ((Number) rows.get(0)[0]).intValue());
+  }
+
+  @Test
+  void testFunctionReturningArrayGivesOneRow() throws Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, false, "$.matrix.first()");
+    assertEquals(1, rows.size());
+    assertEquals(List.of(1, 2), rows.get(0)[0]);
+  }
+
+  @Test
+  void testKeysGivesOneRowForStringAndJsonNodeInput() throws Exception {
+    List<Object[]> stringRows = readRows(ITEMS_JSON, false, "$.obj.keys()");
+    List<Object[]> nodeRows = readNodeRows(ITEMS_JSON, false, "$.obj.keys()");
+    assertEquals(1, stringRows.size());
+    assertEquals(1, nodeRows.size());
+  }
+
+  @Test
+  void testFirstOnEmptyArrayIsMissingValueWhenIgnoringMissingPath() throws 
Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, true, "$.empty.first()");
+    assertEquals(1, rows.size());
+    assertNull(rows.get(0)[0]);
+  }
+
+  @Test
+  void testIndexOutOfRangeIsMissingValueWhenIgnoringMissingPath() throws 
Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, true, "$.matrix.index(5)");
+    assertEquals(1, rows.size());
+    assertNull(rows.get(0)[0]);
+  }
+
+  @Test
+  void testFailingFunctionReportsCauseWhenNotIgnoringMissingPath() {
+    JsonInputException e =
+        assertThrows(JsonInputException.class, () -> readRows(ITEMS_JSON, 
false, "$.empty.sum()"));
+    assertTrue(e.getMessage().contains("$.empty.sum()"), e.getMessage());
+    assertTrue(e.getMessage().contains("empty array"), e.getMessage());
+  }
+
+  @Test
+  void testFirstOnEmptyArrayFailsWhenNotIgnoringMissingPath() {
+    assertThrows(JsonInputException.class, () -> readRows(ITEMS_JSON, false, 
"$.empty.first()"));
+  }
+
+  @Test
+  void testMissingFunctionValueNextToMultiRowField() throws Exception {
+    List<Object[]> rows = readRows(ITEMS_JSON, true, "$.items[*].name", 
"$.absent.length()");
+    assertEquals(3, rows.size());
+    assertEquals("abcd", rows.get(2)[0]);
+    assertNull(rows.get(2)[1]);
+  }
+
+  @Test
+  void testFunctionPathWithoutDefaultPathLeafToNull() throws Exception {
+    JsonInputField field = new JsonInputField("value");
+    field.setPath("$.items.length()");
+    FastJsonReader reader =
+        new FastJsonReader(new JsonInputField[] {field}, false, 
mock(ILogChannel.class));
+    List<Object[]> rows =
+        rows(
+            reader.parseStringValue(
+                new 
ByteArrayInputStream(ITEMS_JSON.getBytes(StandardCharsets.UTF_8))));
+    assertEquals(1, rows.size());
+    assertEquals(3, ((Number) rows.get(0)[0]).intValue());
+  }
 }

Reply via email to