Skip to content

Commit b6868bd

Browse files
authored
Merge pull request #37 from rayokota/null-value-option
Option to disable NULL_VALUE output processing
2 parents e73507e + 764bf80 commit b6868bd

3 files changed

Lines changed: 154 additions & 33 deletions

File tree

src/jsonata/jsonata.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1921,6 +1921,7 @@ def __init__(self, expr: Optional[str]) -> None:
19211921

19221922
self.input = None
19231923
self.validate_input = True
1924+
self.output_convert_nulls = True
19241925

19251926
# Note: now and millis are implemented in Functions
19261927
# environment.bind("now", defineFunction(function(picture, timezone) {
@@ -1957,6 +1958,24 @@ def is_validate_input(self) -> bool:
19571958
def set_validate_input(self, validate_input: bool) -> None:
19581959
self.validate_input = validate_input
19591960

1961+
#
1962+
# Checks whether output NULL_VALUE conversion is enabled
1963+
#
1964+
def is_output_convert_nulls(self) -> bool:
1965+
return self.output_convert_nulls
1966+
1967+
#
1968+
# Enable or disable output NULL_VALUE conversion. Enabled by default, which
1969+
# returns both "JSONata null" and "JSONata undefined" as Python None.
1970+
#
1971+
# When disabled, output values may contain Utils.NULL_VALUE indicating
1972+
# "JSONata null" while Python None indicates "JSONata undefined".
1973+
# Manually calling Utils.convert_nulls(result) on a raw result will yield
1974+
# the converted result.
1975+
#
1976+
def set_output_convert_nulls(self, output_convert_nulls: bool) -> None:
1977+
self.output_convert_nulls = output_convert_nulls
1978+
19601979
def evaluate(self, input: Optional[Any], bindings: Optional[Frame] = None) -> Optional[Any]:
19611980
# throw if the expression compiled with syntax errors
19621981
if self.errors is not None:
@@ -1993,7 +2012,8 @@ def evaluate(self, input: Optional[Any], bindings: Optional[Frame] = None) -> Op
19932012
# if (typeof callback === "function") {
19942013
# callback(null, it)
19952014
# }
1996-
it = utils.Utils.convert_nulls(it)
2015+
if self.output_convert_nulls:
2016+
it = utils.Utils.convert_nulls(it)
19972017
return it
19982018
except Exception as err:
19992019
# insert error message into structure

tests/jsonata_test.py

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
import math
44
import pathlib
55
import traceback
6+
import uuid
7+
8+
9+
# Sentinel representing a JSONata undefined result (distinct from JSONata null,
10+
# which surfaces as Python None after Utils.convert_nulls).
11+
UNDEFINED = "__UNDEFINED__" + uuid.uuid4().hex
12+
13+
14+
def evaluate_jsonata(expr, data, bindings):
15+
"""Evaluate `expr`, returning UNDEFINED for an undefined result (to
16+
distinguish it from JSONata null, which becomes Python None)."""
17+
j = jsonata.Jsonata(expr)
18+
j.set_output_convert_nulls(False)
19+
result = j.evaluate(data, bindings)
20+
if result is None:
21+
result = UNDEFINED
22+
return jsonata.Utils.convert_nulls(result)
623

724

825
class TestJsonata:
@@ -27,20 +44,15 @@ def eval_expr(self, expr, data, bindings, expected, code):
2744
for k, v in bindings.items():
2845
binding_frame.bind(k, v)
2946

30-
jsonata_expr = jsonata.Jsonata(expr)
3147
if binding_frame is not None:
3248
binding_frame.set_runtime_bounds(500000 if TestJsonata.debug else 10000, 303)
33-
result = jsonata_expr.evaluate(data, binding_frame)
34-
if code is not None:
35-
success = False
3649

37-
if expected is not None and expected != result:
38-
# if ((""+expected).equals(""+result))
39-
# System.out.println("Value equals failed, stringified equals = true. Result = "+result)
40-
# else
50+
result = evaluate_jsonata(expr, data, binding_frame)
51+
52+
if code is not None:
4153
success = False
4254

43-
if expected is None and result is not None:
55+
if expected != result:
4456
success = False
4557

4658
if TestJsonata.debug and success:
@@ -119,22 +131,6 @@ def run_test_suite(self, name):
119131
success &= self.run_test_case(name, test_case)
120132
return success
121133

122-
def replace_nulls(self, o):
123-
if isinstance(o, list):
124-
index = 0
125-
for i in o:
126-
if i is None:
127-
o[index] = jsonata.Utils.NULL_VALUE
128-
else:
129-
self.replace_nulls(i)
130-
index += 1
131-
if isinstance(o, dict):
132-
for k, v in o.items():
133-
if v is None:
134-
o[k] = jsonata.Utils.NULL_VALUE
135-
else:
136-
self.replace_nulls(v)
137-
138134
testOverrides = None
139135

140136
@staticmethod
@@ -174,20 +170,21 @@ def run_test_case(self, name, test_def):
174170
bindings = test_def.get("bindings")
175171
result = test_def.get("result")
176172

177-
# if (result == null)
178-
# if (testDef.containsKey("result"))
179-
# result = Jsonata.NULL_VALUE
180-
181-
# replaceNulls(result)
173+
# Check if test is expected to return undefined result
174+
if str(test_def.get("undefinedResult")).lower() == "true":
175+
result = UNDEFINED
182176

183177
code = test_def.get("code")
184178

185179
if isinstance(test_def.get("error"), dict):
186180
code = test_def.get("error").get("code")
187181

188-
# System.out.println(""+bindings)
189-
190182
data = test_def.get("data")
183+
# Explicit `"data": null` means JSONata null input (not undefined), so
184+
# feed it as NULL_VALUE since Python None would be read as undefined.
185+
if "data" in test_def and data is None:
186+
data = jsonata.Utils.NULL_VALUE
187+
191188
if data is None and dataset is not None:
192189
data = self.read_json("jsonata/test/test-suite/datasets/" + dataset + ".json")
193190

tests/null_safety_test.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import jsonata
22

3+
from tests.jsonata_test import TestJsonata as JsonataTestRunner, UNDEFINED, evaluate_jsonata
4+
5+
6+
def _execute_jsonata_raw(expr, input):
7+
"""Evaluate without output NULL_VALUE conversion."""
8+
j = jsonata.Jsonata(expr)
9+
j.set_output_convert_nulls(False)
10+
return j.evaluate(input)
11+
312

413
class TestNullSafety:
514

@@ -38,3 +47,98 @@ def test_array_index_preserves_null(self):
3847
data = {"data": [[1, None, 3], [2, None, 4], [3, None, 5]]}
3948
res = jsonata.Jsonata("[$map(data, function($row) { $row[1] })]").evaluate(data)
4049
assert res == [None, None, None]
50+
51+
def test_output_convert_nulls(self):
52+
j = jsonata.Jsonata("$")
53+
j2 = jsonata.Jsonata("$")
54+
j2.set_output_convert_nulls(False)
55+
56+
assert j.is_output_convert_nulls() is True
57+
assert j2.is_output_convert_nulls() is False
58+
59+
res = j.evaluate(jsonata.Utils.NULL_VALUE)
60+
res2 = j2.evaluate(jsonata.Utils.NULL_VALUE)
61+
assert res is None
62+
assert res2 is jsonata.Utils.NULL_VALUE
63+
64+
res = j.evaluate(None)
65+
res2 = j2.evaluate(None)
66+
assert res is None
67+
assert res2 is None
68+
69+
def test_python_null_vs_undefined(self):
70+
test = JsonataTestRunner()
71+
72+
assert test.run_test_case("test-undefined", {
73+
"expr": "undefined",
74+
"undefinedResult": True,
75+
})
76+
77+
assert test.run_test_case("test-null", {
78+
"expr": "null",
79+
"result": None,
80+
})
81+
82+
# raw vs cooked, returning null or undefined
83+
res = _execute_jsonata_raw("null", None)
84+
assert res is jsonata.Utils.NULL_VALUE
85+
86+
res = _execute_jsonata_raw("$", jsonata.Utils.NULL_VALUE)
87+
assert res is jsonata.Utils.NULL_VALUE
88+
89+
res = _execute_jsonata_raw("$", None)
90+
assert res is None
91+
92+
res = _execute_jsonata_raw("no_match", None)
93+
assert res is None
94+
95+
res = evaluate_jsonata("null", None, None)
96+
assert res is None
97+
98+
res = evaluate_jsonata("no_match", None, None)
99+
assert res == UNDEFINED
100+
101+
res = evaluate_jsonata("{}.a", None, None)
102+
assert res == UNDEFINED
103+
104+
res = _execute_jsonata_raw('{"a":null}.a', None)
105+
assert res is jsonata.Utils.NULL_VALUE
106+
107+
res = evaluate_jsonata('{"a":null}.a', None, None)
108+
assert res is None
109+
110+
res = _execute_jsonata_raw('{"a":null}.b', None)
111+
assert res is None
112+
113+
res = evaluate_jsonata('{"a":null}.b', None, None)
114+
assert res == UNDEFINED
115+
116+
res = evaluate_jsonata("[a,null,b][0]", None, None)
117+
assert res is None
118+
119+
res = _execute_jsonata_raw("$[1]", [42, jsonata.Utils.NULL_VALUE])
120+
assert res is jsonata.Utils.NULL_VALUE
121+
122+
res = _execute_jsonata_raw("$[2]", [42, jsonata.Utils.NULL_VALUE])
123+
assert res is None
124+
125+
res = evaluate_jsonata("$[2]", [42, jsonata.Utils.NULL_VALUE], None)
126+
assert res == UNDEFINED
127+
128+
res = jsonata.Jsonata("$").evaluate(jsonata.Utils.NULL_VALUE)
129+
assert res is None
130+
131+
res = _execute_jsonata_raw("{'a':$}", jsonata.Utils.NULL_VALUE)
132+
assert res["a"] is jsonata.Utils.NULL_VALUE
133+
134+
res = _execute_jsonata_raw("{'a':$}", None)
135+
assert res == {}
136+
137+
res = _execute_jsonata_raw("{'a':{'b':$}}", None)
138+
assert res == {"a": {}}
139+
140+
res = _execute_jsonata_raw("[$]", [jsonata.Utils.NULL_VALUE, jsonata.Utils.NULL_VALUE])
141+
assert res == [jsonata.Utils.NULL_VALUE, jsonata.Utils.NULL_VALUE]
142+
143+
res = evaluate_jsonata("[$]", [jsonata.Utils.NULL_VALUE, jsonata.Utils.NULL_VALUE], None)
144+
assert res == [None, None]

0 commit comments

Comments
 (0)