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

bamaer 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 c9872f3727 Issue #4373 : Keep JSON null distinct from an empty string 
(#8691)
c9872f3727 is described below

commit c9872f37277aba3d4d1854dffad63a2b482dcb2e
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 12:01:45 2026 +0200

    Issue #4373 : Keep JSON null distinct from an empty string (#8691)
---
 .../jsoninput/reader/RowOutputConverter.java       |   7 ++
 .../transforms/jsoninput/JsonInputTest.java        | 107 +++++++++++++++++++++
 2 files changed, 114 insertions(+)

diff --git 
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverter.java
 
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverter.java
index 2fd9bd6e23..d10b266987 100644
--- 
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverter.java
+++ 
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverter.java
@@ -51,6 +51,13 @@ public class RowOutputConverter {
     }
     // convert from string
     String strValue = getStringValue(value);
+    if (strValue == null) {
+      // JSON null and a missing path are null. convertDataFromString turns a 
null string into ""
+      // when HOP_EMPTY_STRING_DIFFERS_FROM_NULL is Y, which hides the 
difference between "" and
+      // null. An empty JSON string ("") is not null and still goes through 
conversion below.
+      // See Apache Hop #4373.
+      return null;
+    }
     return targetMeta.convertDataFromString(
         strValue, strConvertMeta, null, null, targetMeta.getTrimType());
   }
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 cff1953d93..da7199790c 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
@@ -19,6 +19,7 @@ package org.apache.hop.pipeline.transforms.jsoninput;
 
 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.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
@@ -42,6 +43,7 @@ import java.util.zip.ZipOutputStream;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.commons.vfs2.FileSystemException;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.HopClientEnvironment;
 import org.apache.hop.core.IRowSet;
 import org.apache.hop.core.exception.HopException;
@@ -1898,4 +1900,109 @@ class JsonInputTest {
       assertTrue(results.contains("three"));
     }
   }
+
+  /**
+   * Issue #4373. With HOP_EMPTY_STRING_DIFFERS_FROM_NULL=Y a JSON {@code 
null} (and a missing path)
+   * must stay null, while {@code ""} stays an empty string. Otherwise both 
come out as "".
+   */
+  @Test
+  void testEmptyStringDiffersFromJsonNull() throws Exception {
+    ParsedRows parsed = readEmptyAndNullRows("Y");
+    List<Object[]> rows = parsed.rows;
+    IValueMeta tenantName = parsed.rowMeta.searchValueMeta("tenantName");
+    // {"params":{},"tenantName":"hop"} — jobNumber is absent
+    assertNull(rows.get(0)[1]);
+    assertEquals("{}", rows.get(0)[2]);
+    assertEquals("hop", rows.get(0)[3]);
+    // {"jobNumber":3,"params":{},"tenantName":null}
+    assertEquals("3", rows.get(1)[1]);
+    assertEquals("{}", rows.get(1)[2]);
+    assertNull(rows.get(1)[3]);
+    assertTrue(tenantName.isNull(rows.get(1)[3]));
+    // {"jobNumber":2,"tenantName":""} — params is absent, tenantName is empty
+    assertEquals("2", rows.get(2)[1]);
+    assertNull(rows.get(2)[2]);
+    assertEquals("", rows.get(2)[3]);
+    assertFalse(tenantName.isNull(rows.get(2)[3]));
+    // {"jobNumber":120,"params":{},"tenantName":"hop"}
+    assertEquals("120", rows.get(3)[1]);
+    assertEquals("{}", rows.get(3)[2]);
+    assertEquals("hop", rows.get(3)[3]);
+    // {"jobNumber":1,"params":{}} — tenantName is absent
+    assertEquals("1", rows.get(4)[1]);
+    assertEquals("{}", rows.get(4)[2]);
+    assertNull(rows.get(4)[3]);
+    assertTrue(tenantName.isNull(rows.get(4)[3]));
+  }
+
+  /**
+   * Default (the variable is N): an empty string is still stored as "", but 
it compares as null, so
+   * preview shows both as {@code <null>}. JSON null and a missing path stay 
null.
+   */
+  @Test
+  void testEmptyStringAndJsonNullLookTheSameByDefault() throws Exception {
+    ParsedRows parsed = readEmptyAndNullRows("N");
+    List<Object[]> rows = parsed.rows;
+    assertNull(rows.get(1)[3]);
+    assertEquals("", rows.get(2)[3]);
+    assertNull(rows.get(0)[1]);
+    assertNull(rows.get(2)[2]);
+    assertNull(rows.get(4)[3]);
+
+    IValueMeta tenantName = parsed.rowMeta.searchValueMeta("tenantName");
+    assertTrue(tenantName.isNull(rows.get(1)[3]));
+    assertTrue(tenantName.isNull(rows.get(2)[3]));
+  }
+
+  private ParsedRows readEmptyAndNullRows(String emptyDiffersFromNull) throws 
Exception {
+    String previous = 
System.getProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL);
+    System.setProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL, 
emptyDiffersFromNull);
+    try {
+      JsonInputField jobNumber = new JsonInputField("jobNumber");
+      jobNumber.setPath("$.jobNumber");
+      jobNumber.setType(IValueMeta.TYPE_STRING);
+      JsonInputField params = new JsonInputField("params");
+      params.setPath("$.params");
+      params.setType(IValueMeta.TYPE_STRING);
+      JsonInputField tenantName = new JsonInputField("tenantName");
+      tenantName.setPath("$.tenantName");
+      tenantName.setType(IValueMeta.TYPE_STRING);
+
+      JsonInputMeta meta = createSimpleMeta("content", jobNumber, params, 
tenantName);
+      JsonInput transform =
+          createJsonInput(
+              "content",
+              meta,
+              new Object[] {"{\"params\":{},\"tenantName\":\"hop\"}"},
+              new Object[] 
{"{\"jobNumber\":3,\"params\":{},\"tenantName\":null}"},
+              new Object[] {"{\"jobNumber\":2,\"tenantName\":\"\"}"},
+              new Object[] 
{"{\"jobNumber\":120,\"params\":{},\"tenantName\":\"hop\"}"},
+              new Object[] {"{\"jobNumber\":1,\"params\":{}}"});
+
+      ParsedRows parsed = new ParsedRows();
+      transform.addRowListener(
+          new RowAdapter() {
+            @Override
+            public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+              parsed.rowMeta = rowMeta;
+              parsed.rows.add(row);
+            }
+          });
+      processRows(transform, 10);
+      assertEquals(0, transform.getErrors());
+      assertEquals(5, parsed.rows.size());
+      return parsed;
+    } finally {
+      if (previous == null) {
+        System.clearProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL);
+      } else {
+        System.setProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL, previous);
+      }
+    }
+  }
+
+  private static final class ParsedRows {
+    private final List<Object[]> rows = new ArrayList<>();
+    private IRowMeta rowMeta;
+  }
 }

Reply via email to