AWS Step Functions で JSONata を使用するデータ変換方法と具体例の紹介

# はじめに
AWS Step Functionsは、サーバレスでワークフローの構築を容易にするサービスです。
Step Functionsがリリースされた当初は、主に複数のAWS Lambdaの関数を順序通り実行するためのサービスとして広まっていきました。
そのため、現在はStep Functionsの標準アクションで実現できる処理が多くなったにも関わらず、今でもLambda関数を作ってデータを渡し、他のサービスの呼び出し(橋渡し)処理やデータの変換処理を実行するアプローチを取られるケースが多々見られます。

このうち、データの変換処理についてはアクションだけで処理するのが難しいことも多かったですが、[2024年11月にStep FunctionsにおけるJSONataのサポートが発表された](https://aws.amazon.com/about-aws/whats-new/2024/11/aws-step-functions-variables-jsonata-transformations/)ことで、Lambda関数を作らずにステートマシン定義内で直接実行できる選択肢が増えています。

この記事では、あるイベントデータを例に、JSONataを使用したデータ変換(文字列操作やフォーマット変換)の具体例を取り上げ、Lambda関数を使わずにJSONataだけで処理する方法に迫りたいと思います。

# JSONataとは
JSONataは、JSONデータのクエリや変換、加工を行うための言語です。
最も分かりやすい特徴が、JSONデータ構造内の値を簡単に選択して参照できる点です。
さらに`$trim()`、`$split()`、`$uppercase()`や`$lowercase()`などの関数を使用して、参照した値を変換することができます。

このJSONataは、Step Functionsの変数定義や出力定義に書くことができ、ステートマシンが受け取った入力データを様々に扱うことを可能としています。

https://docs.jsonata.org/overview.html

# Step Functionsにおける使用方法
Step Functionsのステートマシン定義の中に`QueryLanguage`と呼ばれる属性があり、ここで`JSONata`を指定すると使用できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/879fe4e4-169c-49bb-afbd-29b2cc46684c.png)

現在、新たにステートマシンを作成すると、このJSONataがデフォルトの言語として選ばれるようになっています。
JSONataのサポートが開始されるまでの言語は、JSONPathと呼ばれるものになります。
このJSONPathも組み込み関数を持ちますが、今回解説するJSONataとは記法が異なります。

## クエリ言語を選択的に使用する
ワークフローのトップレベルでJSONataを使用するように設定されている場合、すべてのステートでJSONataを使用する必要があります。
逆に、古くからのステートマシンではワークフローにJSONPathが使用されていますが、この場合、各ステートで個別にJSONPathまたはJSONataを選択して設定できます。
JSONPathで設定されているステートを、JSONataに変更して置き換えることも可能です。
コードを直接編集して、ステートマシン全体および各ステートで使用するクエリ言語を設定することもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/b39d3378-ed05-471a-9e95-f1b6aeacf4de.png)

# JSONataを使用してデータを操作する
ここからは、Step FunctionsでJSONataを使用してデータを変換する様々な例を説明していきます。
この説明で使用するテストデータは以下の通りで、ピンとくる方には分かりますが、DynamoDB Streamsのストリームデータです。

```json
{
  "Records": [
    {
      "eventID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "eventName": "MODIFY",
      "eventVersion": "1.1",
      "eventSource": "aws:dynamodb",
      "awsRegion": "us-west-2",
      "dynamodb": {
        "ApproximateCreationDateTime": 1742610993,
        "Keys": {
          "id": {
            "S": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
          }
        },
        "NewImage": {
          "summary": {
            "S": "Amazon Nova now supports expanded Tool Choice parameter options in the Converse API , enhancing developers' control over model interactions with tools. Today, developers already use the Converse API to create sophisticated conversational applications, such as customized chat bots to maintain conversations over multiple turns. With this update, Nova adds support for 'Any' and 'Tool' modes in addition to the existing 'Auto' mode support, enabling developers to use all three different modes. To learn about expanded Tool Choice parameter support in Amazon Nova's Converse API, see the Amazon Nova user guide. Learn more about Amazon Nova foundation models at the Amazon Nova product page . You can get started with Amazon Nova foundation models in Amazon Bedrock from the Amazon Bedrock console ."
          },
          "link": {
            "S": "https://aws.amazon.com/about-aws/whats-new/2025/03/amazon-nova-expands-tool-converse-api/"
          },
          "id": {
            "S": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
          },
          "published": {
            "S": "2025-03-19"
          },
          "title": {
            "S": "Amazon Nova expands Tool Choice options for Converse API"
          }
        },
        "SequenceNumber": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
        "SizeBytes": 1160,
        "StreamViewType": "NEW_IMAGE"
      },
      "eventSourceARN": "arn:aws:dynamodb:us-west-2:999999999999:table/AWSNewsRSS/stream/2025-03-19T07:14:33.510"
    }
  ]
}
```

このテストデータは、ステートマシンを手動で実行する時に、以下の「入力」に貼り付けて使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/2ff5e0f0-c9f3-438e-8d9c-23388f31cd20.png)

## 使用するアクション
JSONata式は、出力定義と変数定義に書くことが可能です。
どのアクションにも書くことができますが、データの変換処理の所在を明らかにし、ステートマシン全体の見通しを良くするために、特定のアクションから分離されたステートである **「Passステート」を使用することを推奨します。**

Passステートは、ワークフローの編集画面で「フロー」を選ぶことで選択でき、簡単に挿入することが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/4ec70738-1075-408d-af24-a889d60aa044.png)

## 具体例
### 1. データの参照
これが一番簡単で、全ての基本となります。
テストデータの中で、使いたい属性の値だけを選択して参照するために使用します。
階層を辿るように書くだけですので、難しくありません。

Step Functionsでは入力全体を`$states.input`で参照し、これ以降の属性をドットで繋いで参照します。
`{% %}` で囲った文字列の中にJSONata式を書くことで、式に従って値を参照または処理し、属性に代入することができます。

```jsonc
{
    "id": "{% $states.input.Records[0].dynamodb.NewImage.id.S %}",
    "title": "{% $states.input.Records[0].dynamodb.NewImage.title.S %}",
    "summary": "{% $states.input.Records[0].dynamodb.NewImage.summary.S %}",
    "link": "{% $states.input.Records[0].dynamodb.NewImage.link.S %}",
    "published": "{% $states.input.Records[0].dynamodb.NewImage.published.S %}"
}
```

ステートマシンを実行すると、結果の`output`は以下となります。
```json
{
    "id": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
    "title": "Amazon Nova expands Tool Choice options for Converse API",
    "summary": "Amazon Nova now supports expanded Tool Choice parameter options in the Converse API , enhancing developers' control over model interactions with tools. Today, developers already use the Converse API to create sophisticated conversational applications, such as customized chat bots to maintain conversations over multiple turns. With this update, Nova adds support for 'Any' and 'Tool' modes in addition to the existing 'Auto' mode support, enabling developers to use all three different modes. To learn about expanded Tool Choice parameter support in Amazon Nova's Converse API, see the Amazon Nova user guide. Learn more about Amazon Nova foundation models at the Amazon Nova product page . You can get started with Amazon Nova foundation models in Amazon Bedrock from the Amazon Bedrock console .",
    "link": "https://aws.amazon.com/about-aws/whats-new/2025/03/amazon-nova-expands-tool-converse-api/",
    "published": "2025-03-19"
}
```

### 2.文字列の検索と評価
テストデータの`summary`には、記事の概要が書かれています。
JSONata式で`$contains`関数を使えば、この中に特定の文字列が含まれているかどうかを評価することができます。
以下の例では「Converse API」という文字列が含まれているかどうかを評価します。

```json
{
    "containsConverseAPI": "{% $contains($states.input.Records[0].dynamodb.NewImage.summary.S, \"Converse API\") %}"
}
```

ステートマシンを実行すると、結果の`output`は以下となります。

```json
{
    "containsConverseAPI": true
}
```

### 3.文字列の分割
`$split`関数を使用すると、区切り文字で分割することが可能です。
例えば`eventSourceARN`からDynamoDBのテーブル名を抽出します。
ARNを`/`で区切って分割し、得られた配列からテーブル名の部分だけを得ます。

```json
{
    "tableName": "{% $split($states.input.Records[0].eventSourceARN, \"/\")[1]\n %}"
}
```
ステートマシンを実行すると、結果の`output`は以下となります。

```json
{
    "tableName": "AWSNewsRSS"
}
```

### 4.日付に関する処理 (その1)
テストデータに含まれる `ApproximateCreationDateTime` という属性にはUNIXタイムスタンプが含まれています。
これを`yyyy-MM-dd` (例: 2025-03-22) の形式に変換したい時、これまでは、例えばLambda関数を作成し、時刻を扱うライブラリを使って変換していたでしょう。

このUNIXタイムスタンプの変換処理も、JSONata式を使えば、以下のように書くことができます。

```jsonc
{
    "createdDate": "{% $substring($fromMillis($states.input.Records[0].dynamodb.ApproximateCreationDateTime * 1000), 0, 10) %}"
}
```
まず、JSONataの`$fromMillis`関数を使い、UNIXタイムスタンプをISO 8601形式に変換します。
この関数に`$states.input.Records[0].dynamodb.ApproximateCreationDateTime`で参照した値を1000倍して与えれば、見慣れたタイムスタンプが得られます。

https://docs.jsonata.org/date-time-functions#frommillis

得られた結果の先頭から10桁が欲しい文字列なので、同じくJSONataの`$substring`関数で先頭から10桁を取り出します。

https://docs.jsonata.org/string-functions#substring

ステートマシンを実行すると、結果の`output`は以下となります。

```json
{
    "createdDate": "2025-03-22"
}
```

### 5.日付に関する処理 (その2)
さて、上で説明した時刻の変換処理ですが、欲しい文字列が `yyyy-MM-dd hh:mm:ss` (例: 2025-03-22 18:00:00) だった場合を考えてみます。

この時は、以下のように書くことで、期待する形式の文字列を得ることができます。

```json
{
    "createdDate": "{% (\n  $dateString := $fromMillis($states.input.Records[0].dynamodb.ApproximateCreationDateTime * 1000);\n  $substring($dateString, 0, 10) & \" \" & $substring($dateString, 11, 8)\n) %}"
}
```
このJSONata式の文字列は、見やすく整形すると、以下のようになっています。
```
(
    $dateString := $fromMillis($states.input.Records[0].dynamodb.ApproximateCreationDateTime * 1000);
    $substring($dateString, 0, 10) & " " & $substring($dateString, 11, 8)
)
```

JSONata式の特徴として、スコープを定義して内部で変数を持つことが可能で、この式の場合は`$dateString`という変数に`$fromMillis`関数の結果を代入しておき、後続の処理で使用しています。
また、`&`を使うと、関数の実行結果で得られた文字列の結合が簡単にできます。

マネジメントコンソールでは、このような複雑な数式を簡単に入力することが可能です。
まず属性の値に `{% %}` を入力すると、鉛筆マークが出現しますので、これを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/1a16cf52-0674-4837-af6d-e45e6b445ae7.png)

表示された編集画面で、先ほどのJSONata式を入力して「変更を適用して閉じる」を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/25abce02-7438-4bbe-b036-99346ecd3a64.png)

適用した結果は以下のようになり、入力した数式が反映されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/f9d946b0-0f52-413f-8c64-6ec8914a8e80.png)

ステートマシンを実行すると、結果の`output`は以下となります。

```json
{
    "createdDate": "2025-03-22 02:36:33"
}
```

### 6.さらに複雑な処理を書く
これまでの処理で日付の文字列を得ることができましたが、例えば、曜日の情報が欲しい場合はどうすれば良いでしょうか。
実は、このような場合においても、JSONata式にはプログラムのように長い処理を書くことができ、さらに数値の演算もできるため、JSONataだけで対応できることがあります。
今回のケースにおいて`{% %}`に書く処理は、次のようになります。

```
(
    $dateString := $fromMillis($states.input.Records[0].dynamodb.ApproximateCreationDateTime * 1000);
    
    $year := $number($substring($dateString, 0, 4));
    $month := $number($substring($dateString, 5, 2));
    $day := $number($substring($dateString, 8, 2));
    $hour := $substring($dateString, 11, 2);
    $minute := $substring($dateString, 14, 2);
    $second := $substring($dateString, 17, 2);
    $adjMonth := $month >= 3 ? $month : $month + 12;
    $adjYear := $month >= 3 ? $year : $year - 1;
    
    $K := $adjYear % 100;
    $J := $floor($adjYear / 100);
    $h := ($day + $floor((13 * ($adjMonth + 1)) / 5) + $K + $floor($K / 4) + $floor($J / 4) + (5 * $J)) % 7;
  
    $days := ["Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"];
    
    $dayOfWeek := $days[$h];
    
    $substring($dateString, 0, 10) & " (" & $dayOfWeek & ") " & $hour & ":" & $minute & ":" & $second
)
```
さすがにここまで複雑になるとLambda関数を作ってライブラリを使用した方が早いし楽かもしれませんが、JSONataだけでここまで複雑な処理ができるということと、目の前の複雑そうに見える課題がJSONataだけで解決できるかもしれないと考える選択肢については、覚えておいて損はないでしょう。

ステートマシンを実行すると、結果の`output`は以下となります。

```json
{
    "createdDate": "2025-03-22 (Sat) 02:36:33"
}
```

# JSONataをマネジメントコンソールで書く作業時の工夫

ブラウザの画面で隣にAmazon Qを開きながら、チャットで指示を出して数式を作らせると捗ります。
但し、記事執筆時点(2025年3月)では、この画像のようにマネジメントコンソールのサービス画面で開くAmazon Qでは日本語に対応していないため、注意が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/2f690ae9-f12a-4778-b9e6-cc3bbca2ddc2.png)

# まとめ
本記事では、AWS Step FunctionsにおけるJSONataの活用方法について、頻出するであろう具体例を挙げてご紹介しました。
JSONataを使用すれば、Lambda関数を作らずに、データの変換処理をステートマシンのワークフロー内で完結できることがありますので、ぜひ活用していきましょう。

# 参考文献
* [Transforming data with JSONata in Step Functions](https://docs.aws.amazon.com/step-functions/latest/dg/transforming-data.html)
* [JSONata Documentation](https://docs.jsonata.org/overview.html)