Skip to content

StepFunctions parser: JSONata evaluator is never wired (eval_jsonata_expression calls a None pointer) -> every JSONata expression raises TypeError #10105

Description

@tinovyatkin

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions