diff --git a/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImpl.java index 76853706f6..6d80bf70ab 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImpl.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImpl.java @@ -94,7 +94,7 @@ private static boolean isScalarObject(Object obj) { private static String doJsonize(Object candidate) { if (candidate == null) { - return "null"; // Matches isScalarObject, but not toString-able. + return null; } else if (isScalarObject(candidate)) { return candidate.toString(); } else { diff --git a/core/src/test/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImplTest.java b/core/src/test/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImplTest.java new file mode 100644 index 0000000000..54216aae14 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/function/jsonUDF/JsonExtractFunctionImplTest.java @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.jsonUDF; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class JsonExtractFunctionImplTest { + + @Test + public void testMissingPathReturnsNull() { + Object result = JsonExtractFunctionImpl.eval("{}", "name"); + assertNull(result, "Missing path should return actual null, not string \"null\""); + } + + @Test + public void testExplicitNullValueReturnsNull() { + Object result = JsonExtractFunctionImpl.eval("{\"name\": null}", "name"); + assertNull(result, "Explicit JSON null value should return actual null, not string \"null\""); + } + + @Test + public void testNoArgsReturnsNull() { + assertNull(JsonExtractFunctionImpl.eval()); + } + + @Test + public void testSingleArgReturnsNull() { + assertNull(JsonExtractFunctionImpl.eval("{}")); + } + + @Test + public void testExtractStringValue() { + Object result = JsonExtractFunctionImpl.eval("{\"name\": \"John\"}", "name"); + assertEquals("John", result); + } + + @Test + public void testExtractNumericValue() { + Object result = JsonExtractFunctionImpl.eval("{\"age\": 30}", "age"); + assertEquals("30", result); + } + + @Test + public void testExtractNestedObject() { + Object result = + JsonExtractFunctionImpl.eval("{\"user\": {\"name\": \"John\"}}", "user"); + assertEquals("{\"name\":\"John\"}", result); + } + + @Test + public void testExtractArray() { + Object result = JsonExtractFunctionImpl.eval("{\"items\": [1, 2, 3]}", "items"); + assertEquals("[1,2,3]", result); + } + + @Test + public void testExtractBooleanValue() { + Object result = JsonExtractFunctionImpl.eval("{\"active\": true}", "active"); + assertEquals("true", result); + } + + @Test + public void testMultiPathWithMissingPath() { + Object result = JsonExtractFunctionImpl.eval("{\"name\": \"John\"}", "name", "age"); + assertEquals("[\"John\",null]", result); + } + + @Test + public void testMultiPathAllMissing() { + Object result = JsonExtractFunctionImpl.eval("{}", "name", "age"); + assertEquals("[null,null]", result); + } +} diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJsonBuiltinFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJsonBuiltinFunctionIT.java index ee6ae6ecd4..0ec367aa31 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJsonBuiltinFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJsonBuiltinFunctionIT.java @@ -194,6 +194,36 @@ public void testJsonExtractWithMultiplyResult() throws IOException { verifyDataRows(actual, rows("[[\"Bridge of Sighs\",\"Ponte della Paglia\"],8981.0]")); } + @Test + public void testJsonExtractReturnsNullForMissingPathAndNullValue() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = json_extract('{}', 'name')," + + " b = json_extract('{\\\"name\\\": null}', 'name')" + + " | fields a, b | head 1", + TEST_INDEX_PEOPLE2)); + + verifySchema(actual, schema("a", "string"), schema("b", "string")); + + verifyDataRows(actual, rows(null, null)); + } + + @Test + public void testJsonExtractMultiPathWithMissingPath() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = json_extract('{\\\"name\\\": \\\"John\\\"}'," + + " 'name', 'age')" + + " | fields a | head 1", + TEST_INDEX_PEOPLE2)); + + verifySchema(actual, schema("a", "string")); + + verifyDataRows(actual, rows("[\"John\",null]")); + } + @Test public void testJsonKeys() throws IOException { JSONObject actual =