Step FunctionsのJSONataは、ステートマシンの中で入力を選び、変換するためのクエリ言語です。QueryLanguage に JSONata を指定すると、JSONPathで使っていた5つの入出力フィールドが Arguments と Output の2つにまとまり、Assign で変数も扱えるようになります。この記事では、既存のJSONPath定義を書き換える具体例、Outputを省略したときに入力が消える落とし穴、変数のスコープと上限、式のエラー処理、test-state での1ステートずつの検証、CDKでの書き方までを、そのまま動く定義例とAWS CLIの手順に沿って扱う構成です。仕様と数値は2026年10月時点のAWS公式ドキュメントに基づきます。
まとめ:Step FunctionsのJSONataは新規で採用し既存定義はState単位で移す
新しく作るステートマシンは、トップレベルで "QueryLanguage": "JSONata" を指定して書き始めます。AWSも新規の定義にはJSONataを勧めており、追加料金もかかりません。既存のJSONPath定義は、トップレベルをJSONPathのまま、手を入れるStateから1つずつJSONataへ切り替えるのが安全な順序です。トップレベルをJSONataにすると、JSONPathのStateを混ぜた状態には戻せません。
書き換えで最も事故が起きやすいのは ResultPath の扱いです。JSONataのTaskで Output を省略すると、次のStateに渡るのはタスクの結果だけになり、元の入力は残りません。入力と結果を合わせて渡していた箇所は、$merge で明示的に合成するか、後で使う値を Assign で変数に置いてください。
変換ロジックは1式1秒以内という評価時間の上限があります。重い集計や大きな配列の加工まで式に寄せず、そうした処理はLambdaに残す線引きをしておくと運用で困りません。
JSONataを有効にするQueryLanguageの指定と式を書くときの基本ルール
Step Functionsの全体像や標準・Expressの違いはAWS Step Functionsとは?ステートマシンの仕組み・料金と2種のワークフロー・採用判断を実装者目線で解説で扱っているので、ここでは書き方の規則に絞ります。
QueryLanguageをトップレベルとState単位で指定したときの適用範囲
ステートマシンの構造を定めた公式ドキュメントによると、QueryLanguage の既定値は JSONPath です。トップレベルがJSONPathなら、個々のStateに "QueryLanguage": "JSONata" を書いてそのStateだけを切り替えられます。
| トップレベルの指定 | State側で書ける値 | 使いどころ |
|---|---|---|
| 省略(JSONPath) | JSONPath・JSONata | 既存定義の段階移行 |
| JSONata | JSONataか省略のみ | 新規作成・移行の完了後 |
1つのStateの中で2つの言語を混ぜることはできません。JSONataのStateに Parameters や ResultPath を書くと、作成・更新の時点で検証エラーになります。
式を囲む記号の規則と予約変数$statesで参照できる4つの値
JSONataの式は、文字列を {% と %} で囲んで書きます。JSONataでのデータ変換の公式ドキュメントには、開きの前と閉じの後に空白を入れると検証エラーになるとの記載があります。そのため、式を囲む記号の位置にも注意が必要です。Type やTaskの Resource のように、式を受け付けず定数しか書けないフィールドもあります。
式の中では、予約変数 $states から次の値を参照します。
$states.input:そのStateに入ってきた元の入力(どのフィールドでも参照できる)$states.result:Task・Map・Parallelが成功したときの結果$states.errorOutput:失敗時のエラー出力(Catchの中だけで参照できる)$states.context:実行IDや開始時刻、タスクトークンを含むContextオブジェクト
参照できない場所で $states.result などを書くと、作成・検証の段階で弾かれます。実装はJSONata 2.0.6系の仕様で、$eval は使えず代わりに $parse を使います。関数の一覧を確認する参照先は、JSONataの公式ドキュメントです。
JSONPathの5フィールドをArgumentsとOutputへ書き換える移行の実例
JSONPathでは InputPath・Parameters・ResultSelector・ResultPath・OutputPath の5つで入出力を加工していました。JSONataでは、タスクへ渡す値を Arguments、次のStateへ渡す値を Output に書きます。キー名の末尾に付けていた .$ も不要です。
DynamoDBのGetItemを呼ぶTaskの変換前後の定義と差分
顧客IDで会員ランクを取得し、元の入力に customer として足して次へ渡すTaskを例にします。まずJSONPathの定義です。
"GetCustomer": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:getItem",
"Parameters": {
"TableName": "customers",
"Key": { "customerId": { "S.$": "$.customerId" } }
},
"ResultSelector": {
"name.$": "$.Item.name.S",
"rank.$": "$.Item.rank.S"
},
"ResultPath": "$.customer",
"Next": "CheckRank"
}
同じ処理をJSONataで書くと次のようになります。ResultSelector と ResultPath の2段階の加工が、Output の1つの式にまとまります。
"GetCustomer": {
"Type": "Task",
"QueryLanguage": "JSONata",
"Resource": "arn:aws:states:::dynamodb:getItem",
"Arguments": {
"TableName": "customers",
"Key": { "customerId": { "S": "{% $states.input.customerId %}" } }
},
"Output": "{% $merge([$states.input, {'customer': {'name': $states.result.Item.name.S, 'rank': $states.result.Item.rank.S}}]) %}",
"Next": "CheckRank"
}
式の中でオブジェクトを組み立てるときは、キーを単一引用符で書くとJSONの二重引用符とぶつかりません。$merge は配列内のオブジェクトを1つにまとめ、キーが重なったときは後ろの値で上書きします。
Output省略時は結果だけが残りResultPathの合流が消える失敗パターン
移行で最も多い失敗は、ResultPath を消しただけで Output を書かないケースです。Amazon States Languageの仕様書では、Output を省略したときの出力は、Task・Map・Parallelでは結果、それ以外のStateでは入力と定められています。
先の例で Output を書かないと、次の CheckRank に届くのはDynamoDBの生のレスポンスだけです。元の入力にあった customerId や注文情報は消え、後続のStateが参照した時点で式の評価エラーになります。
移行前の定義から ResultPath を持つStateをすべて拾い、それぞれに Output の $merge か、次章の Assign を当ててください。CatchもOutputを省略するとエラー出力だけが次へ渡ります。
Choiceの比較演算子と組み込み関数をJSONata式へ置き換える対応表
Choiceでは、Variable と NumericGreaterThanEquals などの比較フィールドが、Condition の1つの式に置き換わります。And の入れ子も and・or で1行になります。
"CheckRank": {
"Type": "Choice",
"QueryLanguage": "JSONata",
"Choices": [
{
"Condition": "{% $states.input.customer.rank = 'GOLD' or $states.input.order.total >= 10000 %}",
"Next": "ApplyDiscount"
}
],
"Default": "NoDiscount"
}
States. で始まる組み込み関数は、組み込み関数の公式ドキュメントのとおりJSONPathのStateでしか使えません。JSONataでは標準の関数か、Step Functionsが追加した関数に置き換えます。
| JSONPathの組み込み関数 | JSONataでの書き方 |
|---|---|
| States.Format | & 演算子で連結 |
| States.StringToJson | $parse() |
| States.JsonToString | $string() |
| States.ArrayLength | $count() |
| States.ArrayPartition | $partition() |
| States.ArrayRange | $range() |
| States.Hash | $hash() |
| States.UUID | $uuid() |
| States.MathAdd | + 演算子 |
$partition・$range・$hash・$uuid・$parse の5つはStep Functionsが独自に用意した関数で、JSONata単体の実行環境にはありません。手元のJSONataライブラリで式を試すと、この5つだけ未定義になる点に注意してください。
Assignで変数に値を置き中間のPassステートを削る書き方と動く定義
JSONPathでは、後のStateで使う値を入力に載せたままバケツリレーで運ぶ必要がありました。変数の公式ドキュメントのとおり、Assign に書いた値は以降のどのStateからでも $名前 で参照できます。Assign を書けるのはPass・Task・Map・Parallel・Choice・Waitの各Stateと、Choiceのルール、Catchの中です。
Passだけで動く割引判定のステートマシン定義とAWS CLIでの実行
以下は、注文の合計金額を変数に置き、1万円以上なら1割引きにする定義です。LambdaもDynamoDBも使わないので、実行ロールさえあればそのまま動かせます。order.asl.json として保存してください。
{
"Comment": "JSONataと変数で注文の割引を判定する(Passのみで動く)",
"QueryLanguage": "JSONata",
"StartAt": "SaveOrder",
"States": {
"SaveOrder": {
"Type": "Pass",
"Assign": {
"orderId": "{% $states.input.orderId %}",
"total": "{% $sum($states.input.items.(price * qty)) %}"
},
"Next": "CheckTotal"
},
"CheckTotal": {
"Type": "Choice",
"Choices": [
{ "Condition": "{% $total >= 10000 %}", "Next": "ApplyDiscount" }
],
"Default": "NoDiscount"
},
"ApplyDiscount": {
"Type": "Pass",
"Output": { "orderId": "{% $orderId %}", "total": "{% $total %}", "payable": "{% $floor($total * 0.9) %}" },
"End": true
},
"NoDiscount": {
"Type": "Pass",
"Output": { "orderId": "{% $orderId %}", "total": "{% $total %}", "payable": "{% $total %}" },
"End": true
}
}
}
作成と実行はAWS CLIで行います。アカウントIDとロール名は自分の環境に置き換えます。
aws stepfunctions create-state-machine \
--name jsonata-order-demo \
--definition file://order.asl.json \
--role-arn arn:aws:iam::123456789012:role/StepFunctionsDemoRole \
--region ap-northeast-1
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:ap-northeast-1:123456789012:stateMachine:jsonata-order-demo \
--input '{"orderId":"A-1001","items":[{"sku":"P1","price":4800,"qty":2},{"sku":"P2","price":1200,"qty":1}]}' \
--region ap-northeast-1
# 実行ARNを指定して結果を確認する
aws stepfunctions describe-execution \
--execution-arn 前のコマンドで得たexecutionArn \
--query output --output text \
--region ap-northeast-1
合計は4,800円×2+1,200円で10,800円なので、出力は {"orderId":"A-1001","total":10800,"payable":9720} です。標準ワークフローは状態遷移の回数で課金され、料金ページの米国東部の単価は1,000遷移あたり0.025USDです。中間のPassを1つ削れば、月100万回の実行で100万遷移、約25USDが減る計算になります。
AssignとOutputが同時に評価されるため同じ式を2回書く場面
同じStateの Assign と Output は並列に評価されます。Assign で計算した値はそのStateの Output からは見えず、新しい値が使えるのは次のStateからです。
先の定義で、SaveOrder の Output に "{% $total %}" と書くと失敗します。公式の評価順の説明でも、まだ代入されていない変数を参照すると未定義として失敗するという説明です。同じStateの出力にも合計を載せたいなら、Output 側にも $sum(...) の式をもう一度書きます。
MapとParallelの変数スコープと分散マップから外側を参照できない制約
MapとParallelの中は独立したスコープです。外側の変数は読めますが、他の反復やブランチの変数は読めず、外側と同じ名前への代入も禁止されています。終了すると内側の変数は消えるので、結果は Output で外へ返します。
例外は分散マップ(Distributed Map)です。公式は、分散マップの子ワークフローからは外側のスコープの変数を参照できないと明記しています。インラインMapで $taxRate を中から読んでいた定義を分散マップへ変えると、そのままでは動きません。Mapステート自身の ItemSelector に外側の値を書き、反復ごとの入力として渡します。
"ItemSelector": {
"item": "{% $states.context.Map.Item.Value %}",
"index": "{% $states.context.Map.Item.Index %}",
"taxRate": "{% $taxRate %}"
}
Contextオブジェクトの公式ドキュメントのとおり、Map.Item の値は ItemSelector の中でだけ参照できます。分散マップの設定全般はStep FunctionsのMapステート:インラインと分散マップの違いとS3大量ファイルの並列処理手順で扱っています。
変数1つ256KiB・実行あたり10MiBの上限と大きな結果の置き場所
変数1つの上限は256KiBで、1つの Assign で代入する値の合計も256KiBまでです。1回の実行で保持できる変数の合計は10MiBに制限されます。APIのレスポンスを丸ごと変数に入れていくと、長いワークフローでは10MiBに届きます。
変数には後で使う値だけを置き、大きなデータはS3に書いてキーだけを持ちます。"x.y" のような一部だけの代入はできないため、更新は $merge で作り直した値で変数ごと置き換えます。
JSONata式のエラー処理とtest-stateで1ステートずつ確かめる手順
未定義参照によるStates.QueryEvaluationErrorを防ぐ方法
公式ドキュメントには、式の評価が失敗する原因として次の例が挙げられています。いずれもエラー名は States.QueryEvaluationError です。
- 存在しないフィールドを参照して値が未定義になる(JSONは未定義を表せない)
- 数値でない値どうしの足し算などの型エラー
TimeoutSecondsに文字列や負の数を返すなど、フィールドが受け付けない値- 評価が1秒を超えるタイムアウトと、メモリ上限の超過
- 再帰が深すぎるスタックオーバーフロー
実務で最も多いのは1つ目です。任意項目は $exists で存在を確かめてから参照します。
"Arguments": {
"orderId": "{% $states.input.orderId %}",
"coupon": "{% $exists($states.input.coupon) ? $states.input.coupon : 'NONE' %}"
}
1秒とメモリの上限に当たる処理は、式の書き方で粘らずLambdaへ戻します。数千件の配列を並べ替えて集計するような変換が該当します。
CatchのAssignとOutputで$states.errorOutputを得る定義
Task・Map・ParallelのCatchでは、$states.errorOutput にエラー名(Error)と原因(Cause)が入ります。CatchのAssignは、そのStateを含む外側のスコープの変数に代入できます。
"ChargePayment": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Arguments": {
"FunctionName": "arn:aws:lambda:ap-northeast-1:123456789012:function:charge-payment",
"Payload": { "orderId": "{% $orderId %}", "amount": "{% $payable %}" }
},
"Output": "{% $states.result.Payload %}",
"Catch": [
{
"ErrorEquals": ["States.QueryEvaluationError"],
"Assign": { "failedAt": "{% $states.context.State.Name %}" },
"Output": "{% $merge([$states.input, {'error': $states.errorOutput.Error, 'cause': $states.errorOutput.Cause}]) %}",
"Next": "NotifyDefinitionBug"
},
{
"ErrorEquals": ["States.ALL"],
"Output": "{% $merge([$states.input, {'error': $states.errorOutput.Error}]) %}",
"Next": "NotifyFailure"
}
],
"Next": "Done"
}
式の誤りは定義のバグなので、リトライしても直りません。定義の不具合と外部APIの一時的な失敗を同じCatchで受けると、原因の切り分けが遅れます。エラー処理の公式ドキュメントによると、States.ALL でも States.Runtime と States.DataLimitExceeded は捕まえられないため、Catchで扱えるエラーの範囲にも注意が必要です。Lambda呼び出し側のリトライ設計はStep FunctionsとLambdaの違い:役割の切り分けとlambda:invoke・リトライの実装手順にまとめています。
aws stepfunctions test-stateへ変数と入力を渡す個別検証
JSONataの式は、デプロイせずに test-state で確かめられます。TestState APIのリファレンスによると、variables で変数の値を、stateName でステートマシン定義の中の対象Stateを指定できる仕組みです。実行ロールを渡す roleArn は必須項目ではないため、Passのように他のAWSリソースを呼ばないStateは省略して試せます。
# 1つのStateだけを、変数を与えて評価する
aws stepfunctions test-state \
--definition '{"Type":"Pass","QueryLanguage":"JSONata","Output":{"payable":"{% $floor($total * 0.9) %}"},"End":true}' \
--variables '{"total":10800}' \
--inspection-level DEBUG \
--region ap-northeast-1
# 定義ファイル全体を渡し、CheckTotalの分岐先だけを確かめる
aws stepfunctions test-state \
--definition file://order.asl.json \
--state-name CheckTotal \
--variables '{"orderId":"A-1001","total":9800}' \
--region ap-northeast-1
1つ目は output に {"payable":9720} が返り、2つ目は nextState が NoDiscount になります。DEBUG にすると、Arguments 評価後の値(afterArguments)や変数の内容も inspectionData で確認可能です。移行するStateごとに、移行前の入力を1件流して出力を比べると、Output の書き漏れをデプロイ前に見つけられます。
CDKでJSONataのステートマシンを定義するjsonataメソッドと注意点
CDKそのものの仕組みはAWS CDK(cdk)とは?仕組み・使い方とCloudFormation・Terraformとの違いを参照してください。
jsonataメソッドでLambda呼び出しにassignとoutputsを書く例
aws-cdk-libのStep Functionsモジュールのドキュメントでは、各Stateの生成に jsonata() と jsonPath() を使うよう勧めています。jsonata() のpropsには outputPath などJSONPath用の項目がなく、書き間違いをコンパイル時に検出できます。
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
// priceFn は同じスタックで定義した lambda.Function
const getPrice = tasks.LambdaInvoke.jsonata(this, 'GetPrice', {
lambdaFunction: priceFn,
payload: sfn.TaskInput.fromObject({ sku: '{% $states.input.sku %}' }),
assign: { price: '{% $states.result.Payload.price %}' },
outputs: '{% $states.input %}',
});
const done = sfn.Pass.jsonata(this, 'Done', {
outputs: { sku: '{% $states.input.sku %}', price: '{% $price %}' },
});
new sfn.StateMachine(this, 'PriceStateMachine', {
queryLanguage: sfn.QueryLanguage.JSONATA,
definitionBody: sfn.DefinitionBody.fromChainable(getPrice.next(done)),
});
GetPrice の outputs に入力をそのまま返す式を書いているのは、省略すると次のStateにLambdaの結果だけが渡るためです。前章の失敗パターンはCDKでも同じように起きます。
ステートマシンに QueryLanguage.JSONATA を指定したまま、定義の中に jsonPath() のStateを含めると、CDKはエラーを出します。段階移行の途中は queryLanguage を指定せず(既定のJSONPath)、State側だけを jsonata() に置き換えてください。
JSONataへ移行すべきステートマシンと書き換えを見送るべき場面の判断基準
JSONataとJSONPathは同じステートマシンの中で共存できます。公式ドキュメントも、後方互換のため既定値をJSONPathのままにしています。移行は「直す理由があるStateから」で十分です。
移行すべき場面は、ResultPath と ResultSelector が入り組んで値の出どころを追えない、値を運ぶだけのPassが並んでいる、入力の整形だけのために小さなLambdaを呼んでいる、のどれかに当たる定義です。変数と式に置き換えるとState数と遷移数が減ります。機能追加で触る予定のStateから先に切り替えます。
見送るべき場面もはっきりしています。安定稼働していて今後の変更予定がない定義を、記法を揃える目的だけで一括変換するのは過剰です。テストの無い本番定義を一度に書き換えると、前述の Output の書き漏れが本番で初めて表に出ます。また、数千件の配列の並べ替えや複雑な集計のように1秒の評価上限に近い変換は、JSONataに移さずAWS Lambdaのコードに残すほうが、テストもしやすく安全です。15分を超える処理をコンテナで動かす場合は、Step FunctionsでECSタスクを実行する記事にJSONata形式のrunTask.syncの定義例をまとめています。
状態遷移の設計そのものを見直したい場合は、ステートマシンとは?状態遷移の設計と実装パターン・採用判断を実装者目線で解説が考え方の整理に役立ちます。既存のステートマシンの棚卸しから、移行する範囲の切り分け、CDKやCloudFormationでの管理まで含めて相談したい場合は、インフラ構築(AWS・Google Cloud・Azure)で対応しています。
よくある質問
Step FunctionsのJSONataについて、移行や実装の段階でよく出る質問をまとめました。
Step FunctionsでJSONataを使うと料金は変わりますか?
JSONataと変数そのものに追加料金はかかりません。2024年11月22日のAWS公式ブログでも、追加費用なしと案内されています。値を運ぶだけのPassを変数で削れば状態遷移が減り、Lambdaで行っていた変換を式に移せばLambdaの呼び出し料金が減ります。
JSONPathのステートマシンはいずれ使えなくなりますか?
2026年10月時点の公式ドキュメントに、JSONPathを廃止する記載はありません。新規の定義にはJSONataを勧めつつ、既定値は後方互換のためJSONPathのままとしています。急いで全体を書き換える必要はなく、手を入れるStateから順に切り替える進め方で問題ありません。新規の定義だけは、後から移行の手間が出ないようJSONataで書き始めます。
JSONataの式をローカルで試す方法はありますか?
npmのjsonataライブラリなどで、式の大半は手元で評価できます。ただしStep Functions独自の5関数は未定義になり、$states も存在しないので、入力を置き換えて試す形です。最終確認は、変数と入力を渡せる aws stepfunctions test-state で行うのが確実です。
JSONataのStateでもJSONPathの$.や$$.の書き方は使えますか?
使えません。JSONataのStateでは、入力は $states.input、Contextオブジェクトは $states.context で参照します。$$.Execution.Id に対応する参照式は $states.context.Execution.Id です。なお変数はJSONPathのStateからも $名前 で参照できるため、移行途中ならJSONataのStateが代入した変数をJSONPathのStateで読めます。
コンソールでJSONPathのStateをJSONataに変換できますか?
Step Functionsのコンソールには、JSONPathの個々のStateをJSONataへ変換する操作が用意されています。新規作成時にトップレベルのクエリ言語としてJSONataを選ぶこともできます。ただし変換後の定義は、Output の既定値の違いを踏まえて必ず見直してください。変換したStateは test-state で移行前と同じ入力を流し、出力が一致するかを確かめてから保存すると安全です。
関連記事
- AWS Step Functionsとは?ステートマシンの仕組み・料金と2種のワークフロー・採用判断を実装者目線で解説:標準とExpressの違い・料金の全体像
- Step FunctionsのMapステート:インラインと分散マップの違いとS3大量ファイルの並列処理手順:JSONata形式でのMap定義と分散マップの運用
- Step FunctionsとLambdaの違い:役割の切り分けとlambda:invoke・リトライの実装手順:Lambda呼び出しとRetryの書き方
- AWS CDK(cdk)とは?仕組み・使い方とCloudFormation・Terraformとの違い:ステートマシンをコードで管理する基盤
- ステートマシンとは?状態遷移の設計と実装パターン・採用判断を実装者目線で解説:状態遷移の設計の考え方