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 926aaff6a3 Fix #7990: JSON Input emits string "null" instead of Hop 
null for JSON null values (#7993)
926aaff6a3 is described below

commit 926aaff6a3b2d88cc14fb16b917b13cb76188ca4
Author: kotwal-itpro <[email protected]>
AuthorDate: Tue Aug 18 02:07:28 2026 -0700

    Fix #7990: JSON Input emits string "null" instead of Hop null for JSON null 
values (#7993)
    
    For an incoming JSON payload such as `{"test": null}` with the `test` output
    field declared as type String, the JSON Input transform emitted the four
    character literal string "null" rather than a proper Hop null. This diverged
    from every other type coercion path in the transform, which treats missing
    data as null.
    
    Root cause is in RowOutputConverter.getStringValue(). JsonPath (configured
    via JacksonJsonNodeJsonProvider) parses a JSON null into a Jackson NullNode,
    which is a non-null Java reference whose toString() returns the string 
"null".
    The existing switch matched Map and TextNode explicitly but fell through to
    `jo.toString()` for NullNode, embedding the literal "null" into the output
    row. The downstream `targetMeta.convertDataFromString("null", ...)` then
    happily produced the four-character string on the pipeline.
    
    Detect Jackson NullNode and MissingNode explicitly via `JsonNode.isNull()` 
and
    `JsonNode.isMissingNode()` and return Java null from `getStringValue` in 
both
    cases so `convertDataFromString(null, ...)` produces a proper Hop null.
    MissingNode is included for symmetry with the JsonPath-returned-missing case
    even though the primary bug is triggered by NullNode.
    
    `getStringValue` is now package-private and static so
    `RowOutputConverterTest` can exercise it without spinning up a transform
    context. No other behavior changes: Map, TextNode, plain String, and other
    JsonNode subtypes (IntNode etc.) continue to flow through their existing
    branches unchanged.
    
    Adds RowOutputConverterTest with seven cases: the NullNode and MissingNode
    regressions, the plain Java `null` regression, TextNode unquoted-text path,
    plain-String pass-through, IntNode fall-through, and Map -> JSON 
serialization.
    Full `mvnw test` on plugins/transforms/json passes (117 tests, 0 failures).
    Spotless check clean.
---
 .../jsoninput/reader/RowOutputConverter.java       | 37 ++++++----
 .../jsoninput/reader/RowOutputConverterTest.java   | 80 ++++++++++++++++++++++
 2 files changed, 104 insertions(+), 13 deletions(-)

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 475631627a..2fd9bd6e23 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
@@ -17,6 +17,7 @@
 
 package org.apache.hop.pipeline.transforms.jsoninput.reader;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.TextNode;
 import java.util.Map;
 import net.minidev.json.JSONObject;
@@ -54,20 +55,30 @@ public class RowOutputConverter {
         strValue, strConvertMeta, null, null, targetMeta.getTrimType());
   }
 
-  private String getStringValue(Object jo) {
-    String nodevalue = null;
-    if (jo != null) {
-      if (jo instanceof Map) {
-        Map<String, ?> asStrMap = (Map<String, ?>) jo;
-        nodevalue = JSONObject.toJSONString(asStrMap);
-      } else if (jo instanceof TextNode jot) {
-        // this avoids returning string enclosed by "" if JsonNode
-        nodevalue = jot.asText();
-      } else {
-        nodevalue = jo.toString();
-      }
+  // Package-private and static so RowOutputConverterTest can exercise it 
directly
+  // without instantiating the surrounding transform.
+  static String getStringValue(Object jo) {
+    if (jo == null) {
+      return null;
+    }
+    // A JSON `null` (e.g. `{"foo": null}`) is parsed by Jackson into a 
NullNode -
+    // a non-null Java reference whose toString() returns the four-character 
literal
+    // "null". Without this branch it flows to jo.toString() at the bottom and 
the
+    // output field ends up containing the string "null" instead of a Hop null.
+    // MissingNode covers the JsonPath-returned-missing case for symmetry.
+    // See Apache Hop #7990.
+    if (jo instanceof JsonNode jsonNode && (jsonNode.isNull() || 
jsonNode.isMissingNode())) {
+      return null;
+    }
+    if (jo instanceof Map) {
+      Map<String, ?> asStrMap = (Map<String, ?>) jo;
+      return JSONObject.toJSONString(asStrMap);
+    }
+    if (jo instanceof TextNode jot) {
+      // this avoids returning string enclosed by "" if JsonNode
+      return jot.asText();
     }
-    return nodevalue;
+    return jo.toString();
   }
 
   public Object[] getRow(Object[] baseOutputRow, Object[] rawPartRow, 
JsonInputData data)
diff --git 
a/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverterTest.java
 
b/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverterTest.java
new file mode 100644
index 0000000000..f3b478a158
--- /dev/null
+++ 
b/plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverterTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.jsoninput.reader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.MissingNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link RowOutputConverter}. */
+class RowOutputConverterTest {
+
+  /**
+   * Regression test for Apache Hop #7990. A JSON {@code null} value is parsed 
by Jackson into a
+   * {@link NullNode}, a non-null Java reference whose {@code toString()} 
returns the string literal
+   * "null". Before the fix, this fell through to the generic {@code 
jo.toString()} branch and
+   * downstream String output fields contained "null" instead of a Hop null.
+   */
+  @Test
+  void getStringValue_nullNode_returnsNull() {
+    assertNull(RowOutputConverter.getStringValue(NullNode.getInstance()));
+  }
+
+  @Test
+  void getStringValue_missingNode_returnsNull() {
+    assertNull(RowOutputConverter.getStringValue(MissingNode.getInstance()));
+  }
+
+  @Test
+  void getStringValue_javaNull_returnsNull() {
+    assertNull(RowOutputConverter.getStringValue(null));
+  }
+
+  @Test
+  void getStringValue_textNode_returnsUnquotedText() {
+    // Ensures the pre-existing TextNode branch still returns the unquoted 
string
+    // rather than the JSON-encoded "\"foo\"" that toString() would produce.
+    assertEquals("foo", 
RowOutputConverter.getStringValue(TextNode.valueOf("foo")));
+  }
+
+  @Test
+  void getStringValue_plainString_isPassedThrough() {
+    assertEquals("plain", RowOutputConverter.getStringValue("plain"));
+  }
+
+  @Test
+  void getStringValue_intNode_fallsThroughToToString() {
+    // Non-Text, non-null Jackson nodes intentionally fall through to 
toString().
+    assertEquals("42", RowOutputConverter.getStringValue(IntNode.valueOf(42)));
+  }
+
+  @Test
+  void getStringValue_map_isSerializedAsJson() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("a", 1);
+    map.put("b", "two");
+    assertEquals("{\"a\":1,\"b\":\"two\"}", 
RowOutputConverter.getStringValue(map));
+  }
+}

Reply via email to