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 e14560239d value mapper fixes and tests, fixes #7005 (#7008)
e14560239d is described below

commit e14560239dfc3076d64447ed3ae208ddc419dfce
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Apr 16 13:57:49 2026 +0200

    value mapper fixes and tests, fixes #7005 (#7008)
---
 ...per.hpl => 0045-value-mapper-default-value.hpl} | 136 +++---
 ...pper.hpl => 0045-value-mapper-empty-string.hpl} | 136 +++---
 integration-tests/transforms/0045-value-mapper.hpl | 140 ++++---
 ...r.csv => golden-value-mapper-default-value.csv} |  19 +-
 ...er.csv => golden-value-mapper-empty-string.csv} |  19 +-
 .../transforms/datasets/golden-value-mapper.csv    |  19 +-
 .../transforms/main-0045-value-mapper.hwf          |  88 ++--
 .../dataset/golden-value-mapper-default-value.json |  40 ++
 .../dataset/golden-value-mapper-empty-string.json  |  40 ++
 .../0045-value-mapper-default-value UNIT.json      |  14 +
 .../0045-value-mapper-empty-string UNIT.json       |  43 ++
 .../transforms/valuemapper/ValueMapper.java        |  85 ++--
 .../transforms/valuemapper/ValueMapperData.java    |   3 +
 .../transforms/valuemapper/ValueMapperDialog.java  |  12 +-
 .../transforms/valuemapper/ValueMapperMeta.java    | 124 ++----
 .../pipeline/transforms/valuemapper/Values.java    |  52 +--
 .../valuemapper/messages/messages_en_US.properties |   2 +
 .../valuemapper/ValueMapperMetaInjectionTest.java  |   3 +
 .../valuemapper/ValueMapperMetaTest.java           | 135 +++++-
 .../transforms/valuemapper/ValueMapperTest.java    | 464 +++++++++++++++++++++
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |   5 +-
 21 files changed, 1131 insertions(+), 448 deletions(-)

diff --git a/integration-tests/transforms/0045-value-mapper.hpl 
b/integration-tests/transforms/0045-value-mapper-default-value.hpl
similarity index 86%
copy from integration-tests/transforms/0045-value-mapper.hpl
copy to integration-tests/transforms/0045-value-mapper-default-value.hpl
index 67639a340e..e39fc37f11 100644
--- a/integration-tests/transforms/0045-value-mapper.hpl
+++ b/integration-tests/transforms/0045-value-mapper-default-value.hpl
@@ -19,81 +19,52 @@ limitations under the License.
 -->
 <pipeline>
   <info>
-    <name>0045-value-mapper</name>
-    <name_sync_with_filename>Y</name_sync_with_filename>
-    <description/>
-    <extended_description/>
-    <pipeline_version/>
+    <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>
     <pipeline_type>Normal</pipeline_type>
     <pipeline_status>0</pipeline_status>
     <parameters>
       <parameter>
         <name>NOT_DEFINED</name>
         <default_value>Not defined</default_value>
-        <description/>
       </parameter>
       <parameter>
         <name>NOT_FOUND</name>
         <default_value>Not found</default_value>
-        <description/>
       </parameter>
     </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>
+    <name>0045-value-mapper</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
     <created_user>-</created_user>
-    <created_date>2022/06/30 15:35:29.264</created_date>
     <modified_user>-</modified_user>
+    <created_date>2022/06/30 15:35:29.264</created_date>
     <modified_date>2022/06/30 15:35:29.264</modified_date>
-    <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
-    <is_key_private>N</is_key_private>
   </info>
-  <notepads>
-  </notepads>
-  <order>
-    <hop>
-      <from>Data grid</from>
-      <to>Value mapper</to>
-      <enabled>Y</enabled>
-    </hop>
-    <hop>
-      <from>Value mapper</from>
-      <to>Dummy</to>
-      <enabled>Y</enabled>
-    </hop>
-  </order>
   <transform>
-    <name>Data grid</name>
     <type>DataGrid</type>
-    <description/>
-    <distribute>N</distribute>
-    <custom_distribution/>
-    <copies>1</copies>
-    <partitioning>
-      <method>none</method>
-      <schema_name/>
-    </partitioning>
+    <name>Data grid</name>
     <fields>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>3</length>
         <name>IATA_Code</name>
-        <precision>-1</precision>
         <type>String</type>
+        <length>3</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>50</length>
         <name>Airport_Name</name>
-        <precision>-1</precision>
         <type>String</type>
+        <length>50</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>2</length>
         <name>Country_Code</name>
-        <precision>-1</precision>
         <type>String</type>
+        <length>2</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
     </fields>
     <data>
@@ -137,81 +108,112 @@ limitations under the License.
         <item>Hurghada</item>
         <item>EG</item>
       </line>
+      <line>
+        <item>LIS</item>
+        <item>Humberto Delgado Airport</item>
+        <item>PT</item>
+      </line>
     </data>
-    <attributes/>
+    <distribute>N</distribute>
+    <copies>1</copies>
     <GUI>
       <xloc>96</xloc>
       <yloc>64</yloc>
     </GUI>
-  </transform>
-  <transform>
-    <name>Dummy</name>
-    <type>Dummy</type>
-    <description/>
-    <distribute>Y</distribute>
-    <custom_distribution/>
-    <copies>1</copies>
     <partitioning>
       <method>none</method>
       <schema_name/>
     </partitioning>
     <attributes/>
-    <GUI>
-      <xloc>368</xloc>
-      <yloc>64</yloc>
-    </GUI>
   </transform>
   <transform>
-    <name>Value mapper</name>
-    <type>ValueMapper</type>
-    <description/>
+    <type>Dummy</type>
+    <name>Dummy</name>
     <distribute>Y</distribute>
-    <custom_distribution/>
     <copies>1</copies>
+    <GUI>
+      <xloc>368</xloc>
+      <yloc>64</yloc>
+    </GUI>
     <partitioning>
       <method>none</method>
       <schema_name/>
     </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>ValueMapper</type>
+    <name>Value mapper</name>
     <field_to_use>Country_Code</field_to_use>
     <target_field>Country_Name</target_field>
-    <non_match_default>[${NOT_FOUND}]</non_match_default>
+    <non_match_default>[${NOT_DEFINED}]</non_match_default>
+    <target_type/>
     <fields>
       <field>
-        <source_value/>
         <target_value>[${NOT_DEFINED}]</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>BE</source_value>
         <target_value>Belgium</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>CA</source_value>
         <target_value>Canada</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>FR</source_value>
         <target_value>France</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>UK</source_value>
         <target_value>United Kingdom</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>US</source_value>
         <target_value>United States</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>DE</source_value>
         <target_value>German</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
+      </field>
+      <field>
+        <source_value>PT</source_value>
+        <target_value/>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
     </fields>
-    <attributes/>
+    <distribute>Y</distribute>
+    <copies>1</copies>
     <GUI>
       <xloc>240</xloc>
       <yloc>64</yloc>
     </GUI>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
   </transform>
-  <transform_error_handling>
-  </transform_error_handling>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>Value mapper</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Value mapper</from>
+      <to>Dummy</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
   <attributes/>
+  <transform_error_handling/>
 </pipeline>
diff --git a/integration-tests/transforms/0045-value-mapper.hpl 
b/integration-tests/transforms/0045-value-mapper-empty-string.hpl
similarity index 87%
copy from integration-tests/transforms/0045-value-mapper.hpl
copy to integration-tests/transforms/0045-value-mapper-empty-string.hpl
index 67639a340e..c00024b083 100644
--- a/integration-tests/transforms/0045-value-mapper.hpl
+++ b/integration-tests/transforms/0045-value-mapper-empty-string.hpl
@@ -19,81 +19,52 @@ limitations under the License.
 -->
 <pipeline>
   <info>
-    <name>0045-value-mapper</name>
-    <name_sync_with_filename>Y</name_sync_with_filename>
-    <description/>
-    <extended_description/>
-    <pipeline_version/>
+    <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>
     <pipeline_type>Normal</pipeline_type>
     <pipeline_status>0</pipeline_status>
     <parameters>
       <parameter>
         <name>NOT_DEFINED</name>
         <default_value>Not defined</default_value>
-        <description/>
       </parameter>
       <parameter>
         <name>NOT_FOUND</name>
         <default_value>Not found</default_value>
-        <description/>
       </parameter>
     </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>
+    <name>0045-value-mapper</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
     <created_user>-</created_user>
-    <created_date>2022/06/30 15:35:29.264</created_date>
     <modified_user>-</modified_user>
+    <created_date>2022/06/30 15:35:29.264</created_date>
     <modified_date>2022/06/30 15:35:29.264</modified_date>
-    <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
-    <is_key_private>N</is_key_private>
   </info>
-  <notepads>
-  </notepads>
-  <order>
-    <hop>
-      <from>Data grid</from>
-      <to>Value mapper</to>
-      <enabled>Y</enabled>
-    </hop>
-    <hop>
-      <from>Value mapper</from>
-      <to>Dummy</to>
-      <enabled>Y</enabled>
-    </hop>
-  </order>
   <transform>
-    <name>Data grid</name>
     <type>DataGrid</type>
-    <description/>
-    <distribute>N</distribute>
-    <custom_distribution/>
-    <copies>1</copies>
-    <partitioning>
-      <method>none</method>
-      <schema_name/>
-    </partitioning>
+    <name>Data grid</name>
     <fields>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>3</length>
         <name>IATA_Code</name>
-        <precision>-1</precision>
         <type>String</type>
+        <length>3</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>50</length>
         <name>Airport_Name</name>
-        <precision>-1</precision>
         <type>String</type>
+        <length>50</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>2</length>
         <name>Country_Code</name>
-        <precision>-1</precision>
         <type>String</type>
+        <length>2</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
     </fields>
     <data>
@@ -137,81 +108,112 @@ limitations under the License.
         <item>Hurghada</item>
         <item>EG</item>
       </line>
+      <line>
+        <item>LIS</item>
+        <item>Humberto Delgado Airport</item>
+        <item>PT</item>
+      </line>
     </data>
-    <attributes/>
+    <distribute>N</distribute>
+    <copies>1</copies>
     <GUI>
       <xloc>96</xloc>
       <yloc>64</yloc>
     </GUI>
-  </transform>
-  <transform>
-    <name>Dummy</name>
-    <type>Dummy</type>
-    <description/>
-    <distribute>Y</distribute>
-    <custom_distribution/>
-    <copies>1</copies>
     <partitioning>
       <method>none</method>
       <schema_name/>
     </partitioning>
     <attributes/>
-    <GUI>
-      <xloc>368</xloc>
-      <yloc>64</yloc>
-    </GUI>
   </transform>
   <transform>
-    <name>Value mapper</name>
-    <type>ValueMapper</type>
-    <description/>
+    <type>Dummy</type>
+    <name>Dummy</name>
     <distribute>Y</distribute>
-    <custom_distribution/>
     <copies>1</copies>
+    <GUI>
+      <xloc>368</xloc>
+      <yloc>64</yloc>
+    </GUI>
     <partitioning>
       <method>none</method>
       <schema_name/>
     </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>ValueMapper</type>
+    <name>Value mapper</name>
     <field_to_use>Country_Code</field_to_use>
     <target_field>Country_Name</target_field>
-    <non_match_default>[${NOT_FOUND}]</non_match_default>
+    <non_match_default/>
+    <target_type/>
     <fields>
       <field>
-        <source_value/>
         <target_value>[${NOT_DEFINED}]</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>BE</source_value>
         <target_value>Belgium</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>CA</source_value>
         <target_value>Canada</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>FR</source_value>
         <target_value>France</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>UK</source_value>
         <target_value>United Kingdom</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>US</source_value>
         <target_value>United States</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
       </field>
       <field>
         <source_value>DE</source_value>
         <target_value>German</target_value>
+        <empty_string_equals_null>Y</empty_string_equals_null>
+      </field>
+      <field>
+        <source_value>PT</source_value>
+        <target_value/>
+        <empty_string_equals_null>N</empty_string_equals_null>
       </field>
     </fields>
-    <attributes/>
+    <distribute>Y</distribute>
+    <copies>1</copies>
     <GUI>
       <xloc>240</xloc>
       <yloc>64</yloc>
     </GUI>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
   </transform>
-  <transform_error_handling>
-  </transform_error_handling>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>Value mapper</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Value mapper</from>
+      <to>Dummy</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
   <attributes/>
+  <transform_error_handling/>
 </pipeline>
diff --git a/integration-tests/transforms/0045-value-mapper.hpl 
b/integration-tests/transforms/0045-value-mapper.hpl
index 67639a340e..7001a17580 100644
--- a/integration-tests/transforms/0045-value-mapper.hpl
+++ b/integration-tests/transforms/0045-value-mapper.hpl
@@ -19,81 +19,64 @@ limitations under the License.
 -->
 <pipeline>
   <info>
-    <name>0045-value-mapper</name>
-    <name_sync_with_filename>Y</name_sync_with_filename>
-    <description/>
-    <extended_description/>
-    <pipeline_version/>
+    <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>
     <pipeline_type>Normal</pipeline_type>
     <pipeline_status>0</pipeline_status>
     <parameters>
       <parameter>
         <name>NOT_DEFINED</name>
         <default_value>Not defined</default_value>
-        <description/>
       </parameter>
       <parameter>
         <name>NOT_FOUND</name>
         <default_value>Not found</default_value>
-        <description/>
       </parameter>
     </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>
+    <name>0045-value-mapper</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
     <created_user>-</created_user>
-    <created_date>2022/06/30 15:35:29.264</created_date>
     <modified_user>-</modified_user>
+    <created_date>2022/06/30 15:35:29.264</created_date>
     <modified_date>2022/06/30 15:35:29.264</modified_date>
-    <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
-    <is_key_private>N</is_key_private>
   </info>
-  <notepads>
-  </notepads>
-  <order>
-    <hop>
-      <from>Data grid</from>
-      <to>Value mapper</to>
-      <enabled>Y</enabled>
-    </hop>
-    <hop>
-      <from>Value mapper</from>
-      <to>Dummy</to>
-      <enabled>Y</enabled>
-    </hop>
-  </order>
   <transform>
-    <name>Data grid</name>
     <type>DataGrid</type>
-    <description/>
-    <distribute>N</distribute>
-    <custom_distribution/>
-    <copies>1</copies>
-    <partitioning>
-      <method>none</method>
-      <schema_name/>
-    </partitioning>
+    <name>Data grid</name>
     <fields>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>3</length>
+        <currency/>
+        <decimal/>
+        <group/>
         <name>IATA_Code</name>
-        <precision>-1</precision>
         <type>String</type>
+        <format/>
+        <length>3</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>50</length>
+        <currency/>
+        <decimal/>
+        <group/>
         <name>Airport_Name</name>
-        <precision>-1</precision>
         <type>String</type>
+        <format/>
+        <length>50</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
       <field>
-        <set_empty_string>N</set_empty_string>
-        <length>2</length>
+        <currency/>
+        <decimal/>
+        <group/>
         <name>Country_Code</name>
-        <precision>-1</precision>
         <type>String</type>
+        <format/>
+        <length>2</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
       </field>
     </fields>
     <data>
@@ -137,47 +120,48 @@ limitations under the License.
         <item>Hurghada</item>
         <item>EG</item>
       </line>
+      <line>
+        <item>LIS</item>
+        <item>Humberto Delgado Airport</item>
+        <item>PT</item>
+      </line>
     </data>
-    <attributes/>
+    <distribute>N</distribute>
+    <copies>1</copies>
     <GUI>
       <xloc>96</xloc>
       <yloc>64</yloc>
     </GUI>
-  </transform>
-  <transform>
-    <name>Dummy</name>
-    <type>Dummy</type>
-    <description/>
-    <distribute>Y</distribute>
-    <custom_distribution/>
-    <copies>1</copies>
     <partitioning>
       <method>none</method>
       <schema_name/>
     </partitioning>
     <attributes/>
-    <GUI>
-      <xloc>368</xloc>
-      <yloc>64</yloc>
-    </GUI>
   </transform>
   <transform>
-    <name>Value mapper</name>
-    <type>ValueMapper</type>
-    <description/>
+    <type>Dummy</type>
+    <name>Dummy</name>
     <distribute>Y</distribute>
-    <custom_distribution/>
     <copies>1</copies>
+    <GUI>
+      <xloc>368</xloc>
+      <yloc>64</yloc>
+    </GUI>
     <partitioning>
       <method>none</method>
       <schema_name/>
     </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>ValueMapper</type>
+    <name>Value mapper</name>
     <field_to_use>Country_Code</field_to_use>
     <target_field>Country_Name</target_field>
-    <non_match_default>[${NOT_FOUND}]</non_match_default>
+    <non_match_default/>
+    <target_type/>
     <fields>
       <field>
-        <source_value/>
         <target_value>[${NOT_DEFINED}]</target_value>
       </field>
       <field>
@@ -204,14 +188,36 @@ limitations under the License.
         <source_value>DE</source_value>
         <target_value>German</target_value>
       </field>
+      <field>
+        <source_value>PT</source_value>
+        <target_value/>
+      </field>
     </fields>
-    <attributes/>
+    <distribute>Y</distribute>
+    <copies>1</copies>
     <GUI>
       <xloc>240</xloc>
       <yloc>64</yloc>
     </GUI>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
   </transform>
-  <transform_error_handling>
-  </transform_error_handling>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>Value mapper</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Value mapper</from>
+      <to>Dummy</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
   <attributes/>
+  <transform_error_handling/>
 </pipeline>
diff --git a/integration-tests/transforms/datasets/golden-value-mapper.csv 
b/integration-tests/transforms/datasets/golden-value-mapper-default-value.csv
similarity index 83%
copy from integration-tests/transforms/datasets/golden-value-mapper.csv
copy to 
integration-tests/transforms/datasets/golden-value-mapper-default-value.csv
index 23edc538fc..9887eb65b2 100644
--- a/integration-tests/transforms/datasets/golden-value-mapper.csv
+++ 
b/integration-tests/transforms/datasets/golden-value-mapper-default-value.csv
@@ -1,9 +1,10 @@
-IATA_Code,Airport_Name,Country_Code,Country_Name
-JFK,John F. Kennedy International Airport,US,United States
-LAX,Los Angeles International Airport,,[Not defined]
-ORD,O'Hare International Airport,US,United States
-YYZ,Lester B. Pearson International Airport,CA,Canada
-LHR,Heathrow Airport,UK,United Kingdom
-BRU,Brussels Airport,BE,Belgium
-CDG,Paris Charles de Gaulle Airport,FR,France
-HRG,Hurghada,EG,[Not found]
+IATA_Code,Airport_Name,Country_Code,Country_Name
+JFK,John F. Kennedy International Airport,US,United States
+LAX,Los Angeles International Airport,,[Not defined]
+ORD,O'Hare International Airport,US,United States
+YYZ,Lester B. Pearson International Airport,CA,Canada
+LHR,Heathrow Airport,UK,United Kingdom
+BRU,Brussels Airport,BE,Belgium
+CDG,Paris Charles de Gaulle Airport,FR,France
+HRG,Hurghada,EG,[Not defined]
+LIS,Humberto Delgado Airport,PT,
diff --git a/integration-tests/transforms/datasets/golden-value-mapper.csv 
b/integration-tests/transforms/datasets/golden-value-mapper-empty-string.csv
similarity index 86%
copy from integration-tests/transforms/datasets/golden-value-mapper.csv
copy to 
integration-tests/transforms/datasets/golden-value-mapper-empty-string.csv
index 23edc538fc..eec98c8322 100644
--- a/integration-tests/transforms/datasets/golden-value-mapper.csv
+++ b/integration-tests/transforms/datasets/golden-value-mapper-empty-string.csv
@@ -1,9 +1,10 @@
-IATA_Code,Airport_Name,Country_Code,Country_Name
-JFK,John F. Kennedy International Airport,US,United States
-LAX,Los Angeles International Airport,,[Not defined]
-ORD,O'Hare International Airport,US,United States
-YYZ,Lester B. Pearson International Airport,CA,Canada
-LHR,Heathrow Airport,UK,United Kingdom
-BRU,Brussels Airport,BE,Belgium
-CDG,Paris Charles de Gaulle Airport,FR,France
-HRG,Hurghada,EG,[Not found]
+IATA_Code,Airport_Name,Country_Code,Country_Name
+JFK,John F. Kennedy International Airport,US,United States
+LAX,Los Angeles International Airport,,[Not defined]
+ORD,O'Hare International Airport,US,United States
+YYZ,Lester B. Pearson International Airport,CA,Canada
+LHR,Heathrow Airport,UK,United Kingdom
+BRU,Brussels Airport,BE,Belgium
+CDG,Paris Charles de Gaulle Airport,FR,France
+HRG,Hurghada,EG,
+LIS,Humberto Delgado Airport,PT,
diff --git a/integration-tests/transforms/datasets/golden-value-mapper.csv 
b/integration-tests/transforms/datasets/golden-value-mapper.csv
index 23edc538fc..eec98c8322 100644
--- a/integration-tests/transforms/datasets/golden-value-mapper.csv
+++ b/integration-tests/transforms/datasets/golden-value-mapper.csv
@@ -1,9 +1,10 @@
-IATA_Code,Airport_Name,Country_Code,Country_Name
-JFK,John F. Kennedy International Airport,US,United States
-LAX,Los Angeles International Airport,,[Not defined]
-ORD,O'Hare International Airport,US,United States
-YYZ,Lester B. Pearson International Airport,CA,Canada
-LHR,Heathrow Airport,UK,United Kingdom
-BRU,Brussels Airport,BE,Belgium
-CDG,Paris Charles de Gaulle Airport,FR,France
-HRG,Hurghada,EG,[Not found]
+IATA_Code,Airport_Name,Country_Code,Country_Name
+JFK,John F. Kennedy International Airport,US,United States
+LAX,Los Angeles International Airport,,[Not defined]
+ORD,O'Hare International Airport,US,United States
+YYZ,Lester B. Pearson International Airport,CA,Canada
+LHR,Heathrow Airport,UK,United Kingdom
+BRU,Brussels Airport,BE,Belgium
+CDG,Paris Charles de Gaulle Airport,FR,France
+HRG,Hurghada,EG,
+LIS,Humberto Delgado Airport,PT,
diff --git a/integration-tests/transforms/main-0045-value-mapper.hwf 
b/integration-tests/transforms/main-0045-value-mapper.hwf
index 73b34d7a01..dcb2c3927e 100644
--- a/integration-tests/transforms/main-0045-value-mapper.hwf
+++ b/integration-tests/transforms/main-0045-value-mapper.hwf
@@ -20,88 +20,87 @@ limitations under the License.
 <workflow>
   <name>main-0045-value-mapper</name>
   <name_sync_with_filename>Y</name_sync_with_filename>
-  <description/>
-  <extended_description/>
-  <workflow_version/>
   <created_user>-</created_user>
-  <created_date>2022/04/29 10:42:16.470</created_date>
   <modified_user>-</modified_user>
+  <created_date>2022/04/29 10:42:16.470</created_date>
   <modified_date>2022/04/29 10:42:16.470</modified_date>
-  <parameters>
-    </parameters>
+  <parameters/>
   <actions>
     <action>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <DayOfMonth>1</DayOfMonth>
+      <weekDay>1</weekDay>
+      <minutes>0</minutes>
+      <hour>12</hour>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
       <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>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
     <action>
-      <name>Run ValueMapper Unit Test</name>
-      <description/>
-      <type>RunPipelineTests</type>
-      <attributes/>
       <test_names>
         <test_name>
           <name>0045-value-mapper UNIT</name>
         </test_name>
+        <test_name>
+          <name>0045-value-mapper-default-value UNIT</name>
+        </test_name>
+        <test_name>
+          <name>0045-value-mapper-empty-string UNIT</name>
+        </test_name>
       </test_names>
-      <parallel>N</parallel>
+      <name>Run ValueMapper Unit Test</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
       <xloc>448</xloc>
       <yloc>48</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
     <action>
-      <name>0045-value-mapper-output-type.hpl</name>
-      <description/>
-      <type>PIPELINE</type>
-      <attributes/>
-      <add_date>N</add_date>
-      <add_time>N</add_time>
-      <clear_files>N</clear_files>
+      <filename>${PROJECT_HOME}/0045-value-mapper-output-type.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>
-      <exec_per_row>N</exec_per_row>
-      <filename>${PROJECT_HOME}/0045-value-mapper-output-type.hpl</filename>
-      <logext/>
-      <logfile/>
+      <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>
-      <params_from_previous>N</params_from_previous>
       <run_configuration>local</run_configuration>
-      <set_append_logfile>N</set_append_logfile>
-      <set_logfile>N</set_logfile>
-      <wait_until_finished>Y</wait_until_finished>
-      <parallel>N</parallel>
+      <name>0045-value-mapper-output-type.hpl</name>
+      <type>PIPELINE</type>
+      <attributes/>
       <xloc>224</xloc>
       <yloc>48</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
     <action>
+      <message>Something wrong happened!</message>
+      <always_log_rows>N</always_log_rows>
       <name>Error</name>
       <description/>
       <type>ABORT</type>
       <attributes/>
-      <always_log_rows>N</always_log_rows>
-      <message>Something wrong happened!</message>
-      <parallel>N</parallel>
       <xloc>224</xloc>
       <yloc>176</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
   </actions>
@@ -109,26 +108,25 @@ limitations under the License.
     <hop>
       <from>Start</from>
       <to>0045-value-mapper-output-type.hpl</to>
-      <enabled>Y</enabled>
       <evaluation>Y</evaluation>
       <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
     </hop>
     <hop>
       <from>0045-value-mapper-output-type.hpl</from>
       <to>Run ValueMapper Unit Test</to>
-      <enabled>Y</enabled>
       <evaluation>Y</evaluation>
       <unconditional>N</unconditional>
+      <enabled>Y</enabled>
     </hop>
     <hop>
       <from>0045-value-mapper-output-type.hpl</from>
       <to>Error</to>
-      <enabled>Y</enabled>
       <evaluation>N</evaluation>
       <unconditional>N</unconditional>
+      <enabled>Y</enabled>
     </hop>
   </hops>
-  <notepads>
-  </notepads>
+  <notepads/>
   <attributes/>
 </workflow>
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-value-mapper-default-value.json
 
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-default-value.json
new file mode 100644
index 0000000000..aa4281363f
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-default-value.json
@@ -0,0 +1,40 @@
+{
+  "base_filename": "golden-value-mapper-default-value.csv",
+  "name": "golden-value-mapper-default-value",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": 3,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "IATA_Code",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 50,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "Airport_Name",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 2,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "Country_Code",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 16,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "Country_Name",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-value-mapper-empty-string.json
 
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-empty-string.json
new file mode 100644
index 0000000000..f3fc65d92c
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-empty-string.json
@@ -0,0 +1,40 @@
+{
+  "base_filename": "golden-value-mapper-empty-string.csv",
+  "name": "golden-value-mapper-empty-string",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": 3,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "IATA_Code",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 50,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "Airport_Name",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 2,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "Country_Code",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": 16,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "Country_Name",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0045-value-mapper-default-value
 UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-default-value
 UNIT.json
new file mode 100644
index 0000000000..bad180479d
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-default-value
 UNIT.json 
@@ -0,0 +1,14 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "${HOP_UNIT_TESTS_FOLDER}",
+  "golden_data_sets": [],
+  "input_data_sets": [],
+  "name": "0045-value-mapper-default-value UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0045-value-mapper-default-value.hpl"
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0045-value-mapper-empty-string
 UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-empty-string
 UNIT.json
new file mode 100644
index 0000000000..19f226aa6a
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-empty-string
 UNIT.json  
@@ -0,0 +1,43 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "${HOP_UNIT_TESTS_FOLDER}",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "IATA_Code",
+          "data_set_field": "IATA_Code"
+        },
+        {
+          "transform_field": "Airport_Name",
+          "data_set_field": "Airport_Name"
+        },
+        {
+          "transform_field": "Country_Code",
+          "data_set_field": "Country_Code"
+        },
+        {
+          "transform_field": "Country_Name",
+          "data_set_field": "Country_Name"
+        }
+      ],
+      "field_order": [
+        "IATA_Code",
+        "Airport_Name",
+        "Country_Code",
+        "Country_Name"
+      ],
+      "data_set_name": "golden-value-mapper-empty-string",
+      "transform_name": "Dummy"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0045-value-mapper-empty-string UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0045-value-mapper-empty-string.hpl"
+}
\ No newline at end of file
diff --git 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
index 3472fdb1d6..fc63af53e4 100644
--- 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
+++ 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
@@ -35,6 +35,7 @@ import org.apache.hop.pipeline.transform.TransformMeta;
 /** Convert Values in a certain fields to other values */
 public class ValueMapper extends BaseTransform<ValueMapperMeta, 
ValueMapperData> {
   private static final Class<?> PKG = ValueMapperMeta.class;
+  public static final String CONST_STRING = "String";
 
   private boolean nonMatchActivated = false;
 
@@ -99,19 +100,18 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
     // Use only normal storage type in the HashMap
     Object sourceData = 
data.sourceValueMeta.convertToNormalStorageType(sourceValue);
     Object target = null;
-
-    // Null/Empty mapping to value...
-    //
-    if (data.emptyFieldValue != null && (r[data.keynr] == null || sourceData 
== null)) {
-      target = data.emptyFieldValue; // that's all there is to it.
-    } else {
-      if (sourceData != null) {
-        if (data.mapValues.containsKey(sourceData)) {
-          target = data.mapValues.get(sourceData);
-        } else if (nonMatchActivated) {
-          // If we have a nonMatchDefault and don't have a match
-          target = data.nonMatchDefault;
-        }
+    boolean mapped = false;
+
+    if (data.emptySourceMappingDefined && (r[data.keynr] == null || sourceData 
== null)) {
+      target = data.emptyFieldValue;
+      mapped = true;
+    } else if (sourceData != null) {
+      if (data.mapValues.containsKey(sourceData)) {
+        target = data.mapValues.get(sourceData);
+        mapped = true;
+      } else if (nonMatchActivated) {
+        target = data.nonMatchDefault;
+        mapped = true;
       }
     }
 
@@ -121,8 +121,7 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
       // Did we find anything to map to?
       r[data.outputMeta.size() - 1] = target;
     } else {
-      // Don't set the original value to null if we don't have a target.
-      if (target != null) {
+      if (mapped) {
         r[data.keynr] = target;
       } else {
         // Convert to normal storage type.
@@ -156,7 +155,8 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
   /** Build the value map, default-on-nonmatch, and empty/null mapping. */
   private void builMapValues() throws HopException {
 
-    IValueMeta stringValueMeta = new ValueMetaString("String");
+    data.emptySourceMappingDefined = false;
+    IValueMeta stringValueMeta = new ValueMetaString(CONST_STRING);
     // --- Default for non-match --------------
     //
     try {
@@ -190,14 +190,18 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
         throw new HopValueException(msg, ce);
       }
       Object tgtValue;
-      try {
-        tgtValue = keyFromString(data.targetValueMeta, stringValueMeta, tgt);
-      } catch (HopValueException ce) {
-        String msg =
-            String.format(
-                "Mapping entries: cannot convert target [%s] to target type 
[%s].",
-                src, typeName(data.sourceValueMeta));
-        throw new HopValueException(msg, ce);
+      if (v.isEmptyStringEqualsNull() && Utils.isEmpty(tgt)) {
+        tgtValue = null;
+      } else {
+        try {
+          tgtValue = keyFromString(data.targetValueMeta, stringValueMeta, tgt);
+        } catch (HopValueException ce) {
+          String msg =
+              String.format(
+                  "Mapping entries: cannot convert target [%s] to target type 
[%s].",
+                  src, typeName(data.sourceValueMeta));
+          throw new HopValueException(msg, ce);
+        }
       }
 
       if (srcValue != null) {
@@ -210,10 +214,15 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
     //
     for (Values v : meta.getValues()) {
       if (Utils.isEmpty(v.getSource())) {
-        if (data.emptyFieldValue == null) {
+        if (!data.emptySourceMappingDefined) {
           String emptyFieldString = resolve(v.getTarget());
-          data.emptyFieldValue =
-              keyFromString(data.targetValueMeta, stringValueMeta, 
emptyFieldString);
+          if (v.isEmptyStringEqualsNull() && Utils.isEmpty(emptyFieldString)) {
+            data.emptyFieldValue = null;
+          } else {
+            data.emptyFieldValue =
+                keyFromString(data.targetValueMeta, stringValueMeta, 
emptyFieldString);
+          }
+          data.emptySourceMappingDefined = true;
         } else {
           throw new HopException(
               BaseMessages.getString(
@@ -224,7 +233,8 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
   }
 
   /**
-   * Resolve and set the target output meta type (user-selected, or 
backward-compatible default).
+   * Resolve and set the target output meta type (user-selected, or String 
when unspecified to match
+   * {@link ValueMapperMeta#getFields}).
    */
   private void setTargetMetaType() {
     try {
@@ -234,24 +244,19 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
       if (!Utils.isEmpty(meta.getTargetType())) {
         targetValueMetaName = meta.getTargetType();
         targetValueMetaId = 
ValueMetaFactory.getIdForValueMeta(targetValueMetaName);
+        if (targetValueMetaId == IValueMeta.TYPE_NONE) {
+          targetValueMetaId = IValueMeta.TYPE_STRING;
+          targetValueMetaName = CONST_STRING;
+        }
       } else {
-        // if inputMeta's size == outputMeta's size, the user has not 
specified a new field for the
-        // mapped values
-        //
-        boolean noNewField = data.previousMeta.size() == 
data.outputMeta.size();
-
-        // for backward compatibility:
-        // if the user hasn't specified a new field, the output type is the
-        // same as the source field type, else, is String
-        //
-        targetValueMetaId = noNewField ? data.sourceValueMeta.getType() : 
IValueMeta.TYPE_STRING;
-        targetValueMetaName = data.sourceValueMeta.getName();
+        targetValueMetaId = IValueMeta.TYPE_STRING;
+        targetValueMetaName = CONST_STRING;
       }
 
       data.targetValueMeta =
           ValueMetaFactory.createValueMeta(targetValueMetaName, 
targetValueMetaId);
     } catch (HopException e) {
-      data.targetValueMeta = new ValueMetaString("String");
+      data.targetValueMeta = new ValueMetaString(CONST_STRING);
     }
   }
 
diff --git 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
index c518c9c0f9..8a01440ec3 100644
--- 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
+++ 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
@@ -34,6 +34,9 @@ public class ValueMapperData extends BaseTransformData 
implements ITransformData
   public Object nonMatchDefault;
   public Object emptyFieldValue;
 
+  /** True when the meta defines the single empty-source mapping row (value 
may still be null). */
+  public boolean emptySourceMappingDefined;
+
   public IValueMeta targetValueMeta;
   public IValueMeta outputValueMeta;
   public IValueMeta sourceValueMeta;
diff --git 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
index 90371f785a..e6462c834d 100644
--- 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
+++ 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
@@ -17,6 +17,7 @@
 
 package org.apache.hop.pipeline.transforms.valuemapper;
 
+import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.value.ValueMetaFactory;
@@ -174,7 +175,7 @@ public class ValueMapperDialog extends BaseTransformDialog {
     fdlFields.top = new FormAttachment(wlValueType, margin);
     wlFields.setLayoutData(fdlFields);
 
-    final int FieldsCols = 2;
+    final int FieldsCols = 3;
     final int FieldsRows = input.getValues().size();
 
     ColumnInfo[] colinf = new ColumnInfo[FieldsCols];
@@ -189,6 +190,12 @@ public class ValueMapperDialog extends BaseTransformDialog 
{
             ColumnInfo.COLUMN_TYPE_TEXT,
             false);
     colinf[1].setUsingVariables(true);
+    colinf[2] =
+        new ColumnInfo(
+            BaseMessages.getString(PKG, 
"ValueMapperDialog.Fields.Column.EmptyStringEqualsNull"),
+            ColumnInfo.COLUMN_TYPE_CCOMBO,
+            "Y",
+            "N");
 
     wFields =
         new TableView(
@@ -266,6 +273,7 @@ public class ValueMapperDialog extends BaseTransformDialog {
       if (tgt != null) {
         item.setText(2, tgt);
       }
+      item.setText(3, v.isEmptyStringEqualsNull() ? "Y" : "N");
     }
 
     wFields.setRowNums();
@@ -295,6 +303,8 @@ public class ValueMapperDialog extends BaseTransformDialog {
       Values v = new Values();
       v.setSource(Utils.isEmpty(item.getText(1)) ? null : item.getText(1));
       v.setTarget(item.getText(2));
+      String esn = item.getText(3);
+      v.setEmptyStringEqualsNull(Utils.isEmpty(esn) || Const.toBoolean(esn));
       input.getValues().add(v);
     }
 
diff --git 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
index ec20591c1f..9294d03699 100644
--- 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
+++ 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
@@ -19,6 +19,8 @@ package org.apache.hop.pipeline.transforms.valuemapper;
 
 import java.util.ArrayList;
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.annotations.Transform;
@@ -45,6 +47,8 @@ import org.apache.hop.pipeline.transform.TransformMeta;
     categoryDescription = 
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Transform",
     keywords = "i18n::ValueMapperMeta.keyword",
     documentationUrl = "/pipeline/transforms/valuemapper.html")
+@Getter
+@Setter
 public class ValueMapperMeta extends BaseTransformMeta<ValueMapper, 
ValueMapperData> {
   private static final Class<?> PKG = ValueMapperMeta.class;
 
@@ -100,25 +104,25 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
     }
   }
 
-  /**
-   * @return Returns the fieldValue.
-   */
-  public List<Values> getValues() {
-    return values;
-  }
-
-  /**
-   * @param values The fieldValue to set.
-   */
-  public void setValues(List<Values> values) {
-    this.values = values;
-  }
-
   @Override
   public Object clone() {
     return new ValueMapperMeta(this);
   }
 
+  /** Longest mapped target literal including non-match default (for string 
field sizing). */
+  private int maxLengthOfMappedStringValues() {
+    int maxlen = -1;
+    for (Values v : this.values) {
+      if (v.getTarget() != null && v.getTarget().length() > maxlen) {
+        maxlen = v.getTarget().length();
+      }
+    }
+    if (nonMatchDefault != null && nonMatchDefault.length() > maxlen) {
+      maxlen = nonMatchDefault.length();
+    }
+    return maxlen;
+  }
+
   @Override
   public void getFields(
       IRowMeta r,
@@ -130,7 +134,7 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
 
     IValueMeta extra = null;
 
-    // Determine target value meta type (default to String for backward 
compatibility)
+    // Determine target value meta type (default to String when unspecified)
     String targetTypeName = Utils.isEmpty(getTargetType()) ? "String" : 
getTargetType();
     int targetTypeId = ValueMetaFactory.getIdForValueMeta(targetTypeName);
     // fallback
@@ -149,18 +153,7 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
         // Lengths etc?
         // Take the max length of all the strings...
         //
-        int maxlen = -1;
-        for (Values v : this.values) {
-          if (v.getTarget() != null && v.getTarget().length() > maxlen) {
-            maxlen = v.getTarget().length();
-          }
-        }
-
-        // include default value in max length calculation
-        //
-        if (nonMatchDefault != null && nonMatchDefault.length() > maxlen) {
-          maxlen = nonMatchDefault.length();
-        }
+        int maxlen = maxLengthOfMappedStringValues();
         extra.setLength(maxlen);
         extra.setOrigin(name);
       }
@@ -175,6 +168,25 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
       // The output of a changed field or new field is always a normal storage 
type...
       //
       extra.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+
+      // In-place mapping with no explicit target type: treat output as String 
(same as new field)
+      if (Utils.isEmpty(getTargetField())
+          && Utils.isEmpty(getTargetType())
+          && extra.getType() != IValueMeta.TYPE_STRING) {
+        int idx = r.indexOfValue(extra.getName());
+        r.removeValueMeta(idx);
+        IValueMeta stringMeta;
+        try {
+          stringMeta = ValueMetaFactory.createValueMeta(extra.getName(), 
IValueMeta.TYPE_STRING);
+        } catch (HopPluginException e) {
+          stringMeta = new ValueMetaString(extra.getName());
+        }
+        int maxlen = maxLengthOfMappedStringValues();
+        stringMeta.setLength(maxlen);
+        stringMeta.setOrigin(name);
+        stringMeta.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+        r.addValueMeta(idx, stringMeta);
+      }
     }
   }
 
@@ -229,62 +241,4 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
       remarks.add(cr);
     }
   }
-
-  /**
-   * @return Returns the fieldToUse.
-   */
-  public String getFieldToUse() {
-    return fieldToUse;
-  }
-
-  /**
-   * @param fieldToUse The fieldToUse to set.
-   */
-  public void setFieldToUse(String fieldToUse) {
-    this.fieldToUse = fieldToUse;
-  }
-
-  /**
-   * @return Returns the targetField.
-   */
-  public String getTargetField() {
-    return targetField;
-  }
-
-  /**
-   * @param targetField The targetField to set.
-   */
-  public void setTargetField(String targetField) {
-    this.targetField = targetField;
-  }
-
-  /**
-   * @return the non match default. This is the string that will be used to 
fill in the data when no
-   *     match is found.
-   */
-  public String getNonMatchDefault() {
-    return nonMatchDefault;
-  }
-
-  /**
-   * @param nonMatchDefault the non match default. This is the string that 
will be used to fill in
-   *     the data when no match is found.
-   */
-  public void setNonMatchDefault(String nonMatchDefault) {
-    this.nonMatchDefault = nonMatchDefault;
-  }
-
-  /**
-   * @return Returns the targetType.
-   */
-  public String getTargetType() {
-    return targetType;
-  }
-
-  /**
-   * @param targetType The targetType to set.
-   */
-  public void setTargetType(String targetType) {
-    this.targetType = targetType;
-  }
 }
diff --git 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
index 52863c4791..cf14ba54d2 100644
--- 
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
+++ 
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
@@ -18,9 +18,13 @@
 
 package org.apache.hop.pipeline.transforms.valuemapper;
 
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 
 /** The values for mapping */
+@Getter
+@Setter
 public class Values {
 
   @HopMetadataProperty(
@@ -35,51 +39,27 @@ public class Values {
       injectionKeyDescription = "ValueMapper.Injection.TARGET")
   private String target;
 
+  /**
+   * When true (default), a resolved empty target value is emitted as null 
instead of converting an
+   * empty string to the target type.
+   */
+  @HopMetadataProperty(
+      key = "empty_string_equals_null",
+      injectionKey = "EMPTY_STRING_EQUALS_NULL",
+      injectionKeyDescription = 
"ValueMapper.Injection.EMPTY_STRING_EQUALS_NULL",
+      defaultBoolean = true)
+  private boolean emptyStringEqualsNull = true;
+
   public Values() {}
 
   public Values(Values other) {
     this.source = other.source;
     this.target = other.target;
+    this.emptyStringEqualsNull = other.emptyStringEqualsNull;
   }
 
   public Values(String source, String target) {
     this.source = source;
     this.target = target;
   }
-
-  /**
-   * Gets source value
-   *
-   * @return value of source
-   */
-  public String getSource() {
-    return source;
-  }
-
-  /**
-   * Sets source value
-   *
-   * @param value value of source
-   */
-  public void setSource(String value) {
-    this.source = value;
-  }
-
-  /**
-   * Gets target value
-   *
-   * @return value of source
-   */
-  public String getTarget() {
-    return target;
-  }
-
-  /**
-   * Sets target value
-   *
-   * @param value value of target
-   */
-  public void setTarget(String value) {
-    this.target = value;
-  }
 }
diff --git 
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
 
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
index a3d467f1b7..ff486b5882 100644
--- 
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
+++ 
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
@@ -20,6 +20,7 @@ ValueMapper.Injection.FIELDNAME=The source field to use for 
mapping
 ValueMapper.Injection.NON_MATCH_DEFAULT=The default value if there is not a 
match during lookup
 ValueMapper.Injection.SOURCE=The source values for mapping
 ValueMapper.Injection.TARGET=The target values for mapping
+ValueMapper.Injection.EMPTY_STRING_EQUALS_NULL=When Y, an empty target value 
is emitted as null
 ValueMapper.Injection.TARGET_FIELDNAME=The target field to use for mapping
 ValueMapper.Injection.VALUES=The values for mapping
 ValueMapper.Name=Value mapper
@@ -31,6 +32,7 @@ ValueMapperDialog.FailedToGetFields.DialogTitle=Error
 ValueMapperDialog.FieldnameToUser.Label=Fieldname to use
 ValueMapperDialog.Fields.Column.SourceValue=Source value
 ValueMapperDialog.Fields.Column.TargetValue=Target value
+ValueMapperDialog.Fields.Column.EmptyStringEqualsNull=Empty string equals null
 ValueMapperDialog.Fields.Label=Field values\:
 ValueMapperDialog.NonMatchDefault.Label=Default upon non-matching
 ValueMapperDialog.TargetFieldname.Label=Target field name (empty\=overwrite)
diff --git 
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
index 44b167358d..429aef1dc6 100644
--- 
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
+++ 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
@@ -34,11 +34,14 @@ class ValueMapperMetaInjectionTest extends 
BaseMetadataInjectionTestJunit5<Value
 
   @Test
   void test() throws Exception {
+    meta.getValues().clear();
+    meta.getValues().add(new Values());
     check("FIELDNAME", () -> meta.getFieldToUse());
     check("TARGET_FIELDNAME", () -> meta.getTargetField());
     check("NON_MATCH_DEFAULT", () -> meta.getNonMatchDefault());
     check("SOURCE", () -> meta.getValues().get(0).getSource());
     check("TARGET", () -> meta.getValues().get(0).getTarget());
+    check("EMPTY_STRING_EQUALS_NULL", () -> 
meta.getValues().get(0).isEmptyStringEqualsNull());
     check("TARGET_TYPE", () -> meta.getTargetType());
   }
 }
diff --git 
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
index 97c2aaccfd..f91e1d6f73 100644
--- 
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
+++ 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
@@ -17,15 +17,22 @@
 package org.apache.hop.pipeline.transforms.valuemapper;
 
 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.mockito.Mockito.mock;
 
+import java.util.ArrayList;
+import java.util.List;
 import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.plugins.PluginRegistry;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
 import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
 import org.junit.jupiter.api.BeforeEach;
@@ -40,43 +47,52 @@ class ValueMapperMetaTest {
   }
 
   @Test
-  void testBackwardNoNewFieldNoType() throws Exception {
+  void testInPlaceNoTargetTypeDefaultsToString() throws Exception {
     ValueMapperMeta meta =
         TransformSerializationTestUtil.testSerialization(
             "/value-mapper-transform.xml", ValueMapperMeta.class);
 
-    // backward-compat scenario: in-place mapping, no explicit type
+    // In-place mapping, no explicit target type: output column type is String
     meta.setFieldToUse("Country_Code");
     meta.setTargetField("");
     meta.setTargetType(null);
 
-    // Input schema: only the source field, String
+    // Input schema: numeric codes mapped to country names → output must be 
string
     IRowMeta input = new RowMeta();
-    ValueMetaString src = new ValueMetaString("Country_Code");
-    src.setLength(50);
+    ValueMetaInteger src = new ValueMetaInteger("Country_Code");
     input.addValueMeta(src);
 
-    // target field must not exist in input
     assertNull(input.searchValueMeta("Country_Name"));
 
     IRowMeta out = input.clone();
     meta.getFields(out, "ValueMapper", null, new TransformMeta(), null, null);
 
-    // No new field added
     assertEquals(
         input.size(), out.size(), "Output meta should have same number of 
fields as input");
     assertNull(out.searchValueMeta("Country_Name"), "No target field should be 
added");
 
-    // Same field present with same name and type (String), unchanged length
-    IValueMeta inVm = input.searchValueMeta("Country_Code");
     IValueMeta outVm = out.searchValueMeta("Country_Code");
     assertNotNull(outVm, "Source field must remain present");
-    assertEquals(inVm.getType(), outVm.getType(), "Type should remain 
unchanged in-place");
-    assertEquals(inVm.getName(), outVm.getName(), "Name should remain 
unchanged");
-    assertEquals(inVm.getLength(), outVm.getLength(), "Length should remain 
unchanged");
+    assertEquals(IValueMeta.TYPE_STRING, outVm.getType(), "Unspecified target 
type must be String");
+    assertEquals("Country_Code", outVm.getName(), "Name should remain 
unchanged");
+    assertEquals(16, outVm.getLength(), "Length from longest mapping / default 
literals");
     assertEquals(IValueMeta.STORAGE_TYPE_NORMAL, outVm.getStorageType(), 
"Storage must be NORMAL");
   }
 
+  @Test
+  void legacyXmlMissingEmptyStringEqualsNullDefaultsToTrue() throws Exception {
+    ValueMapperMeta meta =
+        TransformSerializationTestUtil.testSerialization(
+            "/value-mapper-transform.xml", ValueMapperMeta.class);
+
+    for (Values v : meta.getValues()) {
+      assertEquals(
+          true,
+          v.isEmptyStringEqualsNull(),
+          "Older pipelines without empty_string_equals_null must default to Y 
(true)");
+    }
+  }
+
   @Test
   void testSerialization() throws Exception {
     ValueMapperMeta meta =
@@ -112,4 +128,99 @@ class ValueMapperMetaTest {
         out.searchValueMeta("Country_Name").getType(),
         "New target field should default to String type when no explicit 
target type is set");
   }
+
+  @Test
+  void cloneCopiesValuesAndEmptyStringEqualsNull() {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("src");
+    Values v = new Values();
+    v.setSource("x");
+    v.setTarget("y");
+    v.setEmptyStringEqualsNull(false);
+    meta.getValues().add(v);
+
+    ValueMapperMeta clone = (ValueMapperMeta) meta.clone();
+    assertEquals(1, clone.getValues().size());
+    assertFalse(clone.getValues().get(0).isEmptyStringEqualsNull());
+    assertEquals("x", clone.getValues().get(0).getSource());
+    assertEquals("y", clone.getValues().get(0).getTarget());
+  }
+
+  @Test
+  void checkAddsWarningWhenNoPreviousFields() {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    List<ICheckResult> remarks = new ArrayList<>();
+    PipelineMeta pipelineMeta = mock(PipelineMeta.class);
+    TransformMeta transformMeta = mock(TransformMeta.class);
+    meta.check(
+        remarks,
+        pipelineMeta,
+        transformMeta,
+        null,
+        new String[] {"in"},
+        new String[0],
+        null,
+        null,
+        null);
+    assertEquals(2, remarks.size());
+    assertEquals(ICheckResult.TYPE_RESULT_WARNING, remarks.get(0).getType());
+    assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(1).getType());
+  }
+
+  @Test
+  void checkAddsErrorWhenNoInputTransforms() {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    List<ICheckResult> remarks = new ArrayList<>();
+    PipelineMeta pipelineMeta = mock(PipelineMeta.class);
+    TransformMeta transformMeta = mock(TransformMeta.class);
+    IRowMeta prev = new RowMeta();
+    prev.addValueMeta(new ValueMetaString("c"));
+    meta.check(
+        remarks, pipelineMeta, transformMeta, prev, new String[0], new 
String[0], null, null, null);
+    assertEquals(2, remarks.size());
+    assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(0).getType());
+    assertEquals(ICheckResult.TYPE_RESULT_ERROR, remarks.get(1).getType());
+  }
+
+  @Test
+  void getFieldsAddsIntegerTypedTargetColumnWhenSpecified() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("id");
+    meta.setTargetField("amt");
+    meta.setTargetType("Integer");
+    meta.getValues().add(new Values("1", "42"));
+    IRowMeta in = new RowMeta();
+    in.addValueMeta(new ValueMetaString("id"));
+    IRowMeta out = in.clone();
+    meta.getFields(out, "vm", null, new TransformMeta(), null, null);
+    assertEquals(IValueMeta.TYPE_INTEGER, 
out.searchValueMeta("amt").getType());
+  }
+
+  @Test
+  void getFieldsUnknownTargetTypeFallsBackToStringForNewColumn() throws 
Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("id");
+    meta.setTargetField("out");
+    meta.setTargetType("bogus_type_xyz");
+    meta.getValues().add(new Values("a", "b"));
+    IRowMeta in = new RowMeta();
+    in.addValueMeta(new ValueMetaString("id"));
+    IRowMeta out = in.clone();
+    meta.getFields(out, "vm", null, new TransformMeta(), null, null);
+    assertEquals(IValueMeta.TYPE_STRING, out.searchValueMeta("out").getType());
+  }
+
+  @Test
+  void inPlaceWithExplicitTargetTypeDoesNotCoerceColumnInGetFields() throws 
Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType("String");
+    meta.getValues().add(new Values("1", "one"));
+    IRowMeta in = new RowMeta();
+    in.addValueMeta(new ValueMetaInteger("code"));
+    IRowMeta out = in.clone();
+    meta.getFields(out, "vm", null, new TransformMeta(), null, null);
+    assertEquals(IValueMeta.TYPE_INTEGER, 
out.searchValueMeta("code").getType());
+  }
 }
diff --git 
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
new file mode 100644
index 0000000000..44e6cc6592
--- /dev/null
+++ 
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
@@ -0,0 +1,464 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.valuemapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaPluginType;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+@MockitoSettings(strictness = Strictness.LENIENT)
+class ValueMapperTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private TransformMockHelper<ValueMapperMeta, ValueMapperData> helper;
+
+  @BeforeAll
+  static void initHop() throws Exception {
+    HopEnvironment.init();
+    PluginRegistry.init();
+    ValueMetaPluginType.getInstance().searchPlugins();
+  }
+
+  @BeforeEach
+  void setUp() {
+    helper =
+        new TransformMockHelper<>("ValueMapperTest", ValueMapperMeta.class, 
ValueMapperData.class);
+    when(helper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(helper.iLogChannel);
+    when(helper.pipeline.isRunning()).thenReturn(true);
+    when(helper.transformMeta.isPartitioned()).thenReturn(false);
+    helper.pipeline.setVariables(new Variables());
+  }
+
+  @AfterEach
+  void tearDown() {
+    helper.cleanUp();
+  }
+
+  private ValueMapper newSpyTransform(ValueMapperMeta meta, ValueMapperData 
data) {
+    ValueMapper transform =
+        spy(
+            new ValueMapper(
+                helper.transformMeta, meta, data, 0, helper.pipelineMeta, 
helper.pipeline));
+    transform.setMetadataProvider(new MemoryMetadataProvider());
+    data.mapValues = new HashMap<>();
+    return transform;
+  }
+
+  @Test
+  void inPlaceEmptyTargetWithDefaultFlagEmitsNull() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType(null);
+    Values mapRow = new Values();
+    mapRow.setSource("X");
+    mapRow.setTarget("");
+    meta.getValues().add(mapRow);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"X"}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertEquals(1, outputs.size());
+    assertNull(outputs.get(0)[0]);
+    assertFalse(vm.processRow());
+  }
+
+  @Test
+  void inPlaceEmptyTargetWithFlagNoEmitsEmptyString() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType(null);
+    Values mapRow = new Values();
+    mapRow.setSource("X");
+    mapRow.setTarget("");
+    mapRow.setEmptyStringEqualsNull(false);
+    meta.getValues().add(mapRow);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"X"}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertEquals("", outputs.get(0)[0]);
+  }
+
+  @Test
+  void emptySourceWithEmptyTargetAndFlagNEmitsEmptyString() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    Values emptyKey = new Values();
+    emptyKey.setSource(null);
+    emptyKey.setTarget("");
+    emptyKey.setEmptyStringEqualsNull(false);
+    meta.getValues().add(emptyKey);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {null}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertEquals("", outputs.get(0)[0]);
+  }
+
+  @Test
+  void emptySourceMappingWithEmptyTargetAndFlagYEmitsNull() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    Values emptyKey = new Values();
+    emptyKey.setSource(null);
+    emptyKey.setTarget("");
+    meta.getValues().add(emptyKey);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {null}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertNull(outputs.get(0)[0]);
+  }
+
+  @Test
+  void nonMatchDefaultUsedWhenNoMapping() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setNonMatchDefault("DEFAULT");
+    Values mapRow = new Values();
+    mapRow.setSource("A");
+    mapRow.setTarget("B");
+    meta.getValues().add(mapRow);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertEquals("DEFAULT", outputs.get(0)[0]);
+  }
+
+  @Test
+  void newTargetFieldAppendsMappedValue() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("mapped");
+    meta.setTargetType(null);
+    Values mapRow = new Values();
+    mapRow.setSource("A");
+    mapRow.setTarget("B");
+    meta.getValues().add(mapRow);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"A"}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    Object[] out = outputs.get(0);
+    assertEquals("A", out[0]);
+    assertEquals("B", out[1]);
+  }
+
+  @Test
+  void fieldToUseMissingReturnsFalseAndStops() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("missing");
+    meta.setTargetField("");
+    meta.getValues().add(mapping("A", "B"));
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"A"}).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    assertFalse(vm.processRow());
+    verify(helper.pipeline).stopAll();
+  }
+
+  @Test
+  void duplicateEmptySourceRowsThrowHopException() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    Values v1 = new Values();
+    v1.setSource(null);
+    v1.setTarget("a");
+    Values v2 = new Values();
+    v2.setSource(null);
+    v2.setTarget("b");
+    meta.getValues().add(v1);
+    meta.getValues().add(v2);
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {null}).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    assertThrows(HopException.class, vm::processRow);
+  }
+
+  @Test
+  void disposeDoesNotThrow() {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    vm.dispose();
+  }
+
+  @Test
+  void initPreparesMap() {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm =
+        new ValueMapper(helper.transformMeta, meta, data, 0, 
helper.pipelineMeta, helper.pipeline);
+    assertTrue(vm.init());
+    assertTrue(data.mapValues.isEmpty());
+  }
+
+  @Test
+  void inPlaceIntegerSourceMappedToStringValue() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType(null);
+    meta.getValues().add(mapping("1", "one"));
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaInteger("code"));
+    doReturn(new Object[] {1L}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertEquals("one", outputs.get(0)[0]);
+  }
+
+  @Test
+  void invalidNonMatchDefaultForIntegerTargetThrowsHopValueException() throws 
Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType("Integer");
+    meta.setNonMatchDefault("not_an_int");
+    meta.getValues().add(mapping("1", "1"));
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"1"}).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    assertThrows(HopValueException.class, vm::processRow);
+  }
+
+  @Test
+  void invalidMappedSourceForIntegerFieldThrowsHopValueException() throws 
Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType("Integer");
+    meta.getValues().add(mapping("not_an_int", "1"));
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaInteger("code"));
+    doReturn(new Object[] {1L}).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    assertThrows(HopValueException.class, vm::processRow);
+  }
+
+  @Test
+  void invalidMappedTargetForIntegerTypeThrowsHopValueException() throws 
Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType("Integer");
+    meta.getValues().add(mapping("A", "not_an_int"));
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"A"}).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    assertThrows(HopValueException.class, vm::processRow);
+  }
+
+  @Test
+  void unknownTargetTypeFallsBackToStringMeta() throws Exception {
+    ValueMapperMeta meta = new ValueMapperMeta();
+    meta.setFieldToUse("code");
+    meta.setTargetField("");
+    meta.setTargetType("NotARealHopTypeName");
+    meta.getValues().add(mapping("A", "B"));
+
+    ValueMapperData data = new ValueMapperData();
+    ValueMapper vm = newSpyTransform(meta, data);
+    RowMeta inputMeta = new RowMeta();
+    inputMeta.addValueMeta(new ValueMetaString("code"));
+    doReturn(new Object[] {"A"}).doReturn(null).when(vm).getRow();
+    doReturn(inputMeta).when(vm).getInputRowMeta();
+
+    List<Object[]> outputs = new ArrayList<>();
+    doAnswer(
+            inv -> {
+              outputs.add(((Object[]) inv.getArgument(1)).clone());
+              return null;
+            })
+        .when(vm)
+        .putRow(any(IRowMeta.class), any(Object[].class));
+
+    assertTrue(vm.processRow());
+    assertEquals("B", outputs.get(0)[0]);
+  }
+
+  private static Values mapping(String source, String target) {
+    Values v = new Values();
+    v.setSource(source);
+    v.setTarget(target);
+    return v;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index 295e10b086..a7e5b165ee 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -953,9 +953,12 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     }
 
     // If we mousedown on action icon but never committed to drag, treat as 
click (promote for click
-    // handling below)
+    // handling below). Do not promote when this mouseUp is already a 
background click: otherwise
+    // stale currentAction makes an empty-canvas release look like an action 
click and selection is
+    // never cleared.
     if (selectedAction == null
         && currentAction != null
+        && singleClickType != SingleClickType.Workflow
         && startHopAction == null
         && selectionRegion == null
         && selectedActions == null

Reply via email to