Summary
The Step Functions parser backend (execute_state_machine: True) cannot evaluate any JSONata expression: eval_jsonata_expression calls a function pointer that is always None, so every JSONata state fails with TypeError: 'NoneType' object is not callable. This makes all QueryLanguage: JSONATA state machines unusable under the parser backend, regardless of whether a JVM is available.
This is a sibling to #10078 (JSONPath dialect) but a distinct root cause — there the engine is wrong; here the engine is never wired at all.
Root cause
In moto/stepfunctions/parser/asl/jsonata/jsonata.py, the lazy initializer is a no-op — it assigns None to the pointer it is meant to initialize, then immediately calls it:
# moto/stepfunctions/parser/asl/jsonata/jsonata.py (moto 5.1.22)
_eval_jsonata: Optional[Callable[[JSONataExpression], Any]] = None
def eval_jsonata_expression(jsonata_expression: JSONataExpression) -> Any:
global _eval_jsonata
if _eval_jsonata is None:
# Initialize _eval_jsonata only when invoked for the first time using the Singleton pattern.
_eval_jsonata = None # <-- bug: re-assigns None instead of wiring the evaluator
return _eval_jsonata(jsonata_expression) # <-- _eval_jsonata is None -> TypeError
The _JSONataJVMBridge class just above (a jpype/com.dashjoin.jsonata JVM bridge, ported from LocalStack) is presumably what should be assigned here (_eval_jsonata = _JSONataJVMBridge.get().eval_jsonata), but _JSONataJVMBridge.get/.eval_jsonata are never referenced anywhere in moto — the bridge is dead code, so the assignment was dropped in the port.
Note that even if the bridge were wired, it would require a running JVM + jpype + the com.dashjoin.jsonata jar, which is a heavy/awkward dependency for a mocking library and unavailable in many CI/air-gapped environments.
Reproduction
import json, time, os, boto3, moto
from moto import mock_aws
os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1")
DEFN = {
"QueryLanguage": "JSONata",
"StartAt": "Add",
"States": {"Add": {"Type": "Pass", "Output": {"sum": "{% 1 + 1 %}"}, "End": True}},
}
@mock_aws(config={"stepfunctions": {"execute_state_machine": True}})
def run():
sfn = boto3.client("stepfunctions", region_name="us-east-1")
arn = sfn.create_state_machine(
name="jsonata-repro", definition=json.dumps(DEFN),
roleArn="arn:aws:iam::123456789012:role/sf",
)["stateMachineArn"]
ex = sfn.start_execution(stateMachineArn=arn)["executionArn"]
for _ in range(25):
d = sfn.describe_execution(executionArn=ex)
if d["status"] != "RUNNING":
break
time.sleep(0.2)
print("status:", d["status"])
print("cause:", d.get("cause"))
run()
Output (moto 5.1.22):
status: FAILED
cause: TypeError('NoneType' object is not callable)
Expected: the execution SUCCEEDS with output {"sum": 2}.
Any JSONata expression triggers it — {% 1 + 1 %} is about the simplest possible.
Environment
- moto 5.1.22
- Python 3.12
- Step Functions parser backend (
@mock_aws(config={"stepfunctions": {"execute_state_machine": True}}))
Proposed fix
Wire eval_jsonata_expression to a pure-Python JSONata engine instead of the JVM bridge, so the parser has no JVM/jpype dependency. jsonata-python (import jsonata) works well: it is the pure-Python port of the same com.dashjoin/jsonata engine the JVM bridge wraps, so semantics match, and it needs no native wheel.
moto has already composed the full expression string by the time it reaches this seam (( <intrinsic-fn declarations> <$states:=…> <variable stores> <expression> ), with the AWS SFN extension functions $parse/$partition/$range/$hash/$uuid declared inline as ordinary JSONata), so the evaluator only has to run it against an empty input:
from jsonata import Jsonata
def eval_jsonata_expression(jsonata_expression: str) -> Any:
try:
return Jsonata(jsonata_expression).evaluate(None)
except Exception as ex:
raise JSONataException("UNKNOWN", str(ex))
We adopted exactly this in our own test harness (swapping moto's eval_jsonata_expression for a jsonata-python-backed evaluator) and it evaluates the full real-world CDK-generated JSONATA ASL corpus correctly — including moto's own inlined $parse := function($v){$eval($v)} declaration and the $exists(...)/$type(...) validation wrappers. Happy to open a PR along these lines if that's welcome (it would also drop the dead _JSONataJVMBridge / jpype path).
For reference, we filed #10076, #10077, and #10078 for other gaps in the same parser backend while getting CDK-generated state machines to run under moto.
Summary
The Step Functions parser backend (
execute_state_machine: True) cannot evaluate any JSONata expression:eval_jsonata_expressioncalls a function pointer that is alwaysNone, so every JSONata state fails withTypeError: 'NoneType' object is not callable. This makes allQueryLanguage: JSONATAstate machines unusable under the parser backend, regardless of whether a JVM is available.This is a sibling to #10078 (JSONPath dialect) but a distinct root cause — there the engine is wrong; here the engine is never wired at all.
Root cause
In
moto/stepfunctions/parser/asl/jsonata/jsonata.py, the lazy initializer is a no-op — it assignsNoneto the pointer it is meant to initialize, then immediately calls it:The
_JSONataJVMBridgeclass just above (ajpype/com.dashjoin.jsonataJVM bridge, ported from LocalStack) is presumably what should be assigned here (_eval_jsonata = _JSONataJVMBridge.get().eval_jsonata), but_JSONataJVMBridge.get/.eval_jsonataare never referenced anywhere in moto — the bridge is dead code, so the assignment was dropped in the port.Note that even if the bridge were wired, it would require a running JVM +
jpype+ thecom.dashjoin.jsonatajar, which is a heavy/awkward dependency for a mocking library and unavailable in many CI/air-gapped environments.Reproduction
Output (moto 5.1.22):
Expected: the execution SUCCEEDS with output
{"sum": 2}.Any JSONata expression triggers it —
{% 1 + 1 %}is about the simplest possible.Environment
@mock_aws(config={"stepfunctions": {"execute_state_machine": True}}))Proposed fix
Wire
eval_jsonata_expressionto a pure-Python JSONata engine instead of the JVM bridge, so the parser has no JVM/jpypedependency.jsonata-python(import jsonata) works well: it is the pure-Python port of the samecom.dashjoin/jsonataengine the JVM bridge wraps, so semantics match, and it needs no native wheel.moto has already composed the full expression string by the time it reaches this seam (
( <intrinsic-fn declarations> <$states:=…> <variable stores> <expression> ), with the AWS SFN extension functions$parse/$partition/$range/$hash/$uuiddeclared inline as ordinary JSONata), so the evaluator only has to run it against an empty input:We adopted exactly this in our own test harness (swapping moto's
eval_jsonata_expressionfor ajsonata-python-backed evaluator) and it evaluates the full real-world CDK-generated JSONATA ASL corpus correctly — including moto's own inlined$parse := function($v){$eval($v)}declaration and the$exists(...)/$type(...)validation wrappers. Happy to open a PR along these lines if that's welcome (it would also drop the dead_JSONataJVMBridge/jpypepath).For reference, we filed #10076, #10077, and #10078 for other gaps in the same parser backend while getting CDK-generated state machines to run under moto.