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 1d10bd679d do not emit empty row if all files are empty, fixes #8071
(#8091)
1d10bd679d is described below
commit 1d10bd679da7ae418ed8dff7c395888bad0c27c0
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Aug 25 15:23:17 2026 +0200
do not emit empty row if all files are empty, fixes #8071 (#8091)
---
.../json/0013-json-input-ignore-empty-file.hpl | 550 +++++++++++++++++++++
.../json/0014-json-input-fail-on-empty-file.hpl | 123 +++++
.../main-0013-json-input-ignore-empty-file.hwf | 149 ++++++
.../main-0014-json-input-fail-on-empty-file.hwf | 168 +++++++
.../resources/json-input-ignore-empty-file-it.json | 7 +
.../pipeline/transforms/jsoninput/JsonInput.java | 28 +-
.../transforms/jsoninput/reader/InputsReader.java | 9 +-
.../transforms/jsoninput/JsonInputTest.java | 94 ++++
8 files changed, 1124 insertions(+), 4 deletions(-)
diff --git a/integration-tests/json/0013-json-input-ignore-empty-file.hpl
b/integration-tests/json/0013-json-input-ignore-empty-file.hpl
new file mode 100644
index 0000000000..a2e96fd742
--- /dev/null
+++ b/integration-tests/json/0013-json-input-ignore-empty-file.hpl
@@ -0,0 +1,550 @@
+<?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>0013-json-input-ignore-empty-file</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Issue #8071: a zero byte file that is ignored must not
produce an output row</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/08/25 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/25 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Read only the empty file</from>
+ <to>Empty file emitted a row - Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Read good then empty file</from>
+ <to>Count rows of good then empty</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Count rows of good then empty</from>
+ <to>Check good then empty</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Check good then empty</from>
+ <to>Good then empty OK</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Check good then empty</from>
+ <to>Good then empty wrong count - Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Read empty then good file</from>
+ <to>Count rows of empty then good</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Count rows of empty then good</from>
+ <to>Check empty then good</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Check empty then good</from>
+ <to>Empty then good OK</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Check empty then good</from>
+ <to>Empty then good wrong count - Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Read only the empty file</name>
+ <type>JsonInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <include>N</include>
+ <include_field/>
+ <rownum>N</rownum>
+ <rownum_field/>
+ <limit>0</limit>
+ <valueField/>
+ <IsInFields>N</IsInFields>
+ <IsAFile>N</IsAFile>
+ <addresultfile>N</addresultfile>
+ <IsIgnoreEmptyFile>Y</IsIgnoreEmptyFile>
+ <doNotFailIfNoFile>N</doNotFailIfNoFile>
+ <ignoreMissingPath>N</ignoreMissingPath>
+ <readurl>N</readurl>
+ <removeSourceField>N</removeSourceField>
+ <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+ <files>
+ <file>
+ <name>${java.io.tmpdir}/hop-it-0013-empty.json</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ </files>
+ <fields>
+ <field>
+ <name>name</name>
+ <path>$.fighters[*].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>power</name>
+ <path>$.fighters[*].power</path>
+ <type>Integer</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <repeat>N</repeat>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>160</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Empty file emitted a row - Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>The ignored empty file produced an output row</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>384</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Read good then empty file</name>
+ <type>JsonInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <include>N</include>
+ <include_field/>
+ <rownum>N</rownum>
+ <rownum_field/>
+ <limit>0</limit>
+ <valueField/>
+ <IsInFields>N</IsInFields>
+ <IsAFile>N</IsAFile>
+ <addresultfile>N</addresultfile>
+ <IsIgnoreEmptyFile>Y</IsIgnoreEmptyFile>
+ <doNotFailIfNoFile>N</doNotFailIfNoFile>
+ <ignoreMissingPath>N</ignoreMissingPath>
+ <readurl>N</readurl>
+ <removeSourceField>N</removeSourceField>
+ <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+ <files>
+ <file>
+
<name>${PROJECT_HOME}/resources/json-input-ignore-empty-file-it.json</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ <file>
+ <name>${java.io.tmpdir}/hop-it-0013-empty.json</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ </files>
+ <fields>
+ <field>
+ <name>name</name>
+ <path>$.fighters[*].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>power</name>
+ <path>$.fighters[*].power</path>
+ <type>Integer</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <repeat>N</repeat>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>160</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Count rows of good then empty</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <all_rows>N</all_rows>
+ <ignore_aggregate>N</ignore_aggregate>
+ <field_ignore/>
+ <directory>${java.io.tmpdir}</directory>
+ <prefix>grp</prefix>
+ <add_linenr>N</add_linenr>
+ <linenr_fieldname/>
+ <give_back_row>N</give_back_row>
+ <group>
+ </group>
+ <fields>
+ <field>
+ <aggregate>num_rows</aggregate>
+ <subject/>
+ <type>COUNT_ANY</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>384</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Check good then empty</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>Good then empty OK</send_true_to>
+ <send_false_to>Good then empty wrong count - Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>num_rows</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>3</text>
+ <length>-1</length>
+ <precision>0</precision>
+ <isnull>N</isnull>
+ <mask>####0;-####0</mask>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>592</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Good then empty OK</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>816</xloc>
+ <yloc>240</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Good then empty wrong count - Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>Reading a good file followed by an ignored empty file did not
return 3 rows</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>592</xloc>
+ <yloc>352</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Read empty then good file</name>
+ <type>JsonInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <include>N</include>
+ <include_field/>
+ <rownum>N</rownum>
+ <rownum_field/>
+ <limit>0</limit>
+ <valueField/>
+ <IsInFields>N</IsInFields>
+ <IsAFile>N</IsAFile>
+ <addresultfile>N</addresultfile>
+ <IsIgnoreEmptyFile>Y</IsIgnoreEmptyFile>
+ <doNotFailIfNoFile>N</doNotFailIfNoFile>
+ <ignoreMissingPath>N</ignoreMissingPath>
+ <readurl>N</readurl>
+ <removeSourceField>N</removeSourceField>
+ <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+ <files>
+ <file>
+ <name>${java.io.tmpdir}/hop-it-0013-empty.json</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ <file>
+
<name>${PROJECT_HOME}/resources/json-input-ignore-empty-file-it.json</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ </files>
+ <fields>
+ <field>
+ <name>name</name>
+ <path>$.fighters[*].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>power</name>
+ <path>$.fighters[*].power</path>
+ <type>Integer</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <repeat>N</repeat>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>160</xloc>
+ <yloc>448</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Count rows of empty then good</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <all_rows>N</all_rows>
+ <ignore_aggregate>N</ignore_aggregate>
+ <field_ignore/>
+ <directory>${java.io.tmpdir}</directory>
+ <prefix>grp</prefix>
+ <add_linenr>N</add_linenr>
+ <linenr_fieldname/>
+ <give_back_row>N</give_back_row>
+ <group>
+ </group>
+ <fields>
+ <field>
+ <aggregate>num_rows</aggregate>
+ <subject/>
+ <type>COUNT_ANY</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>384</xloc>
+ <yloc>448</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Check empty then good</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>Empty then good OK</send_true_to>
+ <send_false_to>Empty then good wrong count - Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <leftvalue>num_rows</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>3</text>
+ <length>-1</length>
+ <precision>0</precision>
+ <isnull>N</isnull>
+ <mask>####0;-####0</mask>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>592</xloc>
+ <yloc>448</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Empty then good OK</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>816</xloc>
+ <yloc>448</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Empty then good wrong count - Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>Reading an ignored empty file followed by a good file did not
return 3 rows</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>592</xloc>
+ <yloc>560</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/json/0014-json-input-fail-on-empty-file.hpl
b/integration-tests/json/0014-json-input-fail-on-empty-file.hpl
new file mode 100644
index 0000000000..af2daea05a
--- /dev/null
+++ b/integration-tests/json/0014-json-input-fail-on-empty-file.hpl
@@ -0,0 +1,123 @@
+<?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>0014-json-input-fail-on-empty-file</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Issue #8071: without "ignore empty file" a zero byte file
must still be an error</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/08/25 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/25 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Read empty file without ignoring it</from>
+ <to>Rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Read empty file without ignoring it</name>
+ <type>JsonInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <include>N</include>
+ <include_field/>
+ <rownum>N</rownum>
+ <rownum_field/>
+ <limit>0</limit>
+ <valueField/>
+ <IsInFields>N</IsInFields>
+ <IsAFile>N</IsAFile>
+ <addresultfile>N</addresultfile>
+ <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+ <doNotFailIfNoFile>N</doNotFailIfNoFile>
+ <ignoreMissingPath>Y</ignoreMissingPath>
+ <readurl>N</readurl>
+ <removeSourceField>N</removeSourceField>
+ <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+ <files>
+ <file>
+ <name>${java.io.tmpdir}/hop-it-0014-empty.json</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>N</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ </files>
+ <fields>
+ <field>
+ <name>name</name>
+ <path>$.fighters[*].name</path>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <trim_type>none</trim_type>
+ <repeat>N</repeat>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>160</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Rows</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>384</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/json/main-0013-json-input-ignore-empty-file.hwf
b/integration-tests/json/main-0013-json-input-ignore-empty-file.hwf
new file mode 100644
index 0000000000..6eeed1bd70
--- /dev/null
+++ b/integration-tests/json/main-0013-json-input-ignore-empty-file.hwf
@@ -0,0 +1,149 @@
+<?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-0013-json-input-ignore-empty-file</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/25 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/25 10: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>64</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Delete leftover empty file</name>
+ <description/>
+ <type>DELETE_FILE</type>
+ <attributes/>
+ <filename>${java.io.tmpdir}/hop-it-0013-empty.json</filename>
+ <fail_if_file_not_exists>N</fail_if_file_not_exists>
+ <parallel>N</parallel>
+ <xloc>224</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Create empty file</name>
+ <description/>
+ <type>CREATE_FILE</type>
+ <attributes/>
+ <filename>${java.io.tmpdir}/hop-it-0013-empty.json</filename>
+ <fail_if_file_exists>N</fail_if_file_exists>
+ <add_filename_result>N</add_filename_result>
+ <parallel>N</parallel>
+ <xloc>416</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+
<filename>${PROJECT_HOME}/0013-json-input-ignore-empty-file.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <create_parent_folder>N</create_parent_folder>
+ <set_logfile>N</set_logfile>
+ <set_append_logfile>N</set_append_logfile>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <run_configuration>local</run_configuration>
+ <name>0013-json-input-ignore-empty-file.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <xloc>608</xloc>
+ <yloc>64</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Delete empty file</name>
+ <description/>
+ <type>DELETE_FILE</type>
+ <attributes/>
+ <filename>${java.io.tmpdir}/hop-it-0013-empty.json</filename>
+ <fail_if_file_not_exists>N</fail_if_file_not_exists>
+ <parallel>N</parallel>
+ <xloc>816</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Delete leftover empty file</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>Delete leftover empty file</from>
+ <to>Create empty file</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Create empty file</from>
+ <to>0013-json-input-ignore-empty-file.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0013-json-input-ignore-empty-file.hpl</from>
+ <to>Delete empty file</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/json/main-0014-json-input-fail-on-empty-file.hwf
b/integration-tests/json/main-0014-json-input-fail-on-empty-file.hwf
new file mode 100644
index 0000000000..553647f77e
--- /dev/null
+++ b/integration-tests/json/main-0014-json-input-fail-on-empty-file.hwf
@@ -0,0 +1,168 @@
+<?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-0014-json-input-fail-on-empty-file</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>The pipeline is expected to fail: reading a zero byte file
without "ignore empty file" is an error</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/25 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/25 10: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>64</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Delete leftover empty file</name>
+ <description/>
+ <type>DELETE_FILE</type>
+ <attributes/>
+ <filename>${java.io.tmpdir}/hop-it-0014-empty.json</filename>
+ <fail_if_file_not_exists>N</fail_if_file_not_exists>
+ <parallel>N</parallel>
+ <xloc>224</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Create empty file</name>
+ <description/>
+ <type>CREATE_FILE</type>
+ <attributes/>
+ <filename>${java.io.tmpdir}/hop-it-0014-empty.json</filename>
+ <fail_if_file_exists>N</fail_if_file_exists>
+ <add_filename_result>N</add_filename_result>
+ <parallel>N</parallel>
+ <xloc>416</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+
<filename>${PROJECT_HOME}/0014-json-input-fail-on-empty-file.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <create_parent_folder>N</create_parent_folder>
+ <set_logfile>N</set_logfile>
+ <set_append_logfile>N</set_append_logfile>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <run_configuration>local</run_configuration>
+ <name>0014-json-input-fail-on-empty-file.hpl</name>
+ <description>Expected to fail</description>
+ <type>PIPELINE</type>
+ <attributes/>
+ <xloc>608</xloc>
+ <yloc>64</yloc>
+ <parallel>N</parallel>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Empty file did not fail - Abort</name>
+ <description/>
+ <type>ABORT</type>
+ <attributes/>
+ <message>Reading a zero byte file without "ignore empty file" did not
report an error</message>
+ <loglevel>Basic</loglevel>
+ <parallel>N</parallel>
+ <xloc>608</xloc>
+ <yloc>208</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Delete empty file</name>
+ <description/>
+ <type>DELETE_FILE</type>
+ <attributes/>
+ <filename>${java.io.tmpdir}/hop-it-0014-empty.json</filename>
+ <fail_if_file_not_exists>N</fail_if_file_not_exists>
+ <parallel>N</parallel>
+ <xloc>832</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Delete leftover empty file</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>Delete leftover empty file</from>
+ <to>Create empty file</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>Create empty file</from>
+ <to>0014-json-input-fail-on-empty-file.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0014-json-input-fail-on-empty-file.hpl</from>
+ <to>Empty file did not fail - Abort</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0014-json-input-fail-on-empty-file.hpl</from>
+ <to>Delete empty file</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/json/resources/json-input-ignore-empty-file-it.json
b/integration-tests/json/resources/json-input-ignore-empty-file-it.json
new file mode 100644
index 0000000000..3b30d95998
--- /dev/null
+++ b/integration-tests/json/resources/json-input-ignore-empty-file-it.json
@@ -0,0 +1,7 @@
+{
+ "fighters": [
+ { "name": "Goku", "power": 9500 },
+ { "name": "Vegeta", "power": 9000 },
+ { "name": "Gohan", "power": 8500 }
+ ]
+}
diff --git
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInput.java
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInput.java
index 59ecb79ec1..178aab2ef3 100644
---
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInput.java
+++
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInput.java
@@ -29,6 +29,7 @@ import java.util.List;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
+import org.apache.hop.core.IRowSet;
import org.apache.hop.core.QueueRowSet;
import org.apache.hop.core.ResultFile;
import org.apache.hop.core.exception.HopException;
@@ -196,7 +197,12 @@ public class JsonInput extends
BaseFileInputTransform<JsonInputMeta, JsonInputDa
data.inputRowMeta = getInputRowMeta();
if (!meta.isInFields() && data.inputRowMeta == null) {
data.outputRowMeta = new RowMeta();
- if (!meta.isDoNotFailIfNoFile() && (data.files == null ||
data.files.nrOfFiles() == 0)) {
+ if (data.files == null) {
+ // Resolve the file list here: the reader below only does it while
iterating, which is too
+ // late to tell whether there are any files at all.
+ data.files = meta.getFileInputList(this);
+ }
+ if (!meta.isDoNotFailIfNoFile() && data.files.nrOfFiles() == 0) {
String errMsg = BaseMessages.getString(PKG, "JsonInput.Log.NoFiles");
logError(errMsg);
inputError(errMsg);
@@ -319,7 +325,9 @@ public class JsonInput extends
BaseFileInputTransform<JsonInputMeta, JsonInputDa
if (meta.isIgnoringEmptyFile()) {
int first = pb.read();
if (first < 0) {
- data.readerRowSet = data.jsonReader.emptyFieldRowSet();
+ // Empty input while ignoring empty files: a file is skipped without
any output row, a
+ // field value keeps the incoming row with null values.
+ data.readerRowSet = isReadingFiles() ? emptyRowSet() :
data.jsonReader.emptyFieldRowSet();
return;
}
pb.unread(first);
@@ -335,6 +343,18 @@ public class JsonInput extends
BaseFileInputTransform<JsonInputMeta, JsonInputDa
}
}
+ /** Are we reading the JSON from files (file list or file names in a field)?
*/
+ private boolean isReadingFiles() {
+ return !meta.isInFields() || meta.getIsAFile();
+ }
+
+ /** A finished row set without any rows: nothing gets written for this
input. */
+ private IRowSet emptyRowSet() {
+ IRowSet rowSet = new QueueRowSet();
+ rowSet.setDone();
+ return rowSet;
+ }
+
private void parseNextJsonToRowSet(JsonNode node) throws HopException {
try {
data.readerRowSet = data.jsonReader.parseJsonNodeValue(node);
@@ -478,6 +498,10 @@ public class JsonInput extends
BaseFileInputTransform<JsonInputMeta, JsonInputDa
}
BaseTransform.closeQuietly(countingIn);
}
+ } else if (data.skipEmptyFile) {
+ // The last file(s) in the list were empty and are ignored: emit
nothing for them.
+ data.skipEmptyFile = false;
+ data.readerRowSet = emptyRowSet();
} else {
// Null stream (e.g. null field, iterator error path): do not
parse "{}" when ignoring
// empty input — that would still run JSONPath and fail if paths
are missing.
diff --git
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/InputsReader.java
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/InputsReader.java
index a29b9f2e18..1ed99f06eb 100644
---
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/InputsReader.java
+++
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/InputsReader.java
@@ -157,13 +157,18 @@ public class InputsReader implements
Iterable<InputStream> {
}
}
try {
+ JsonInputData jsonData = data instanceof JsonInputData jd ? jd :
null;
+ if (jsonData != null) {
+ jsonData.skipEmptyFile = false;
+ }
data.file = inner.next();
data.currentFileIndex++;
if (transform.onNewFile(data.file)) {
return HopVfs.getInputStream(data.file);
}
- if (data instanceof JsonInputData jsonData &&
jsonData.skipEmptyFile) {
- jsonData.skipEmptyFile = false;
+ if (jsonData != null && jsonData.skipEmptyFile) {
+ // Empty file, ignored: move on to the next one. When there is
none left we return null
+ // with the flag still set, telling the transform not to write a
row for it.
continue;
}
return null;
diff --git
a/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputTest.java
b/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputTest.java
index 1ce8598970..0ba02a6eca 100644
---
a/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputTest.java
+++
b/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputTest.java
@@ -1176,6 +1176,100 @@ class JsonInputTest {
}
}
+ @Test
+ void testZeroSizeFileIgnoredEmitsNoRow() throws Exception {
+ ByteArrayOutputStream err = new ByteArrayOutputStream();
+ helper.redirectLog(err, LogLevel.ERROR);
+ try (FileObject empty = HopVfs.getFileObject(BASE_RAM_DIR + "empty.json"))
{
+ empty.createFile();
+
+ JsonInputField price = new JsonInputField();
+ price.setName("price");
+ price.setType(IValueMeta.TYPE_NUMBER);
+ price.setPath("$..book[*].price");
+
+ JsonInputMeta meta = createFileListMeta(List.of(empty));
+ meta.getInputFields().add(price);
+ meta.setIgnoringEmptyFile(true);
+
+ JsonInput jsonInput = createJsonInput(meta);
+ processRows(jsonInput, 3);
+ disposeJsonInput(jsonInput);
+
+ assertEquals(0, jsonInput.getErrors(), err.toString());
+ assertEquals(0, jsonInput.getLinesWritten(), "rows written");
+ } finally {
+ deleteFiles();
+ }
+ }
+
+ @Test
+ void testZeroSizeFileNotIgnoredIsAnError() throws Exception {
+ ByteArrayOutputStream err = new ByteArrayOutputStream();
+ helper.redirectLog(err, LogLevel.ERROR);
+ try (FileObject empty = HopVfs.getFileObject(BASE_RAM_DIR + "empty.json");
+ LocaleChange enUs = new LocaleChange(Locale.US)) {
+ empty.createFile();
+
+ JsonInputField price = new JsonInputField();
+ price.setName("price");
+ price.setType(IValueMeta.TYPE_NUMBER);
+ price.setPath("$..book[*].price");
+
+ JsonInputMeta meta = createFileListMeta(List.of(empty));
+ meta.getInputFields().add(price);
+ meta.setIgnoringEmptyFile(false);
+ meta.setIgnoringMissingPath(true);
+
+ JsonInput jsonInput = createJsonInput(meta);
+ processRows(jsonInput, 3);
+ disposeJsonInput(jsonInput);
+
+ String logMsgs = err.toString();
+ assertTrue(logMsgs.contains("is empty!"), logMsgs);
+ assertEquals(1, jsonInput.getErrors(), "errors");
+ } finally {
+ deleteFiles();
+ }
+ }
+
+ @Test
+ void testZeroSizeLastFileIgnoredEmitsNoExtraRow() throws Exception {
+ ByteArrayOutputStream err = new ByteArrayOutputStream();
+ helper.redirectLog(err, LogLevel.ERROR);
+ try (FileObject good = HopVfs.getFileObject(BASE_RAM_DIR + "good.json");
+ FileObject empty = HopVfs.getFileObject(BASE_RAM_DIR + "empty.json")) {
+ try (OutputStream os = good.getContent().getOutputStream()) {
+ IOUtils.write(getBasicTestJson(), os, StandardCharsets.UTF_8);
+ }
+ empty.createFile();
+
+ JsonInputField price = new JsonInputField();
+ price.setName("price");
+ price.setType(IValueMeta.TYPE_NUMBER);
+ price.setPath("$..book[*].price");
+
+ JsonInputMeta meta = createFileListMeta(List.of(good, empty));
+ meta.getInputFields().add(price);
+ meta.setIgnoringEmptyFile(true);
+
+ JsonInput jsonInput = createJsonInput(meta);
+ jsonInput.addRowListener(
+ new RowComparatorListener(
+ new Object[] {8.95d},
+ new Object[] {12.99d},
+ new Object[] {8.99d},
+ new Object[] {22.99d}));
+ processRows(jsonInput, 8);
+ disposeJsonInput(jsonInput);
+
+ assertEquals(0, jsonInput.getErrors(), err.toString());
+ assertEquals(4, jsonInput.getLinesWritten(), "rows written");
+ } finally {
+ deleteFiles();
+ }
+ }
+
@Test
void testBracketEscape() throws Exception {
String input = "{\"a\":1,\"b(1)\":2}";