本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
在 Step Functions 中使用 JSONata 转换数据
使用 JSONata,您可以获得一种强大的开源查询和表达式语言,用于在工作流中选择和转换数据。有关简要介绍和完整的 jsonata 参考资料,请参阅JSONata.org 文档。
支持的 jsonata 版本
Step Functions 支持 jsonata 版本 2.0.6。
以下视频以 DynamoDB 为例,描述了 Step Functions 中的变量和 JSONata:
您必须选择加入,以便对现有工作流使用 JSONata 查询和转换语言。在控制台中创建工作流时,建议为顶级状态机 QueryLanguage 选择 JSONata。对于使用 JSONPath 的现有工作流或新工作流,控制台提供了将各个状态转换为 JSONata 的选项。
选择 JSONata 后,您的工作流字段将从五个 JSONPath 字段(InputPath、Parameters、ResultSelector、ResultPath 和 OutputPath)减少到只有两个字段:Arguments 和 Output。此外,您不会在 JSON 对象键名称中使用 .$。
如果您不熟悉 Step Functions,只需知道 JSONata 表达式使用以下语法即可:
JSONata 语法:"{% <JSONata expression> %}"
以下代码示例展示了从 JSONPath 转换为 JSONata 的过程:
# Original sample using JSONPath
{
"QueryLanguage": "JSONPath", // Set explicitly; could be set and inherited from top-level
"Type": "Task",
...
"Parameters": {
"static": "Hello",
"title.$": "$.title",
"name.$": "$customerName", // With $customerName declared as a variable
"not-evaluated": "$customerName"
}
}
# Sample after conversion to JSONata
{
"QueryLanguage": "JSONata", // Set explicitly; could be set and inherited from top-level
"Type": "Task",
...
"Arguments": { // JSONata states do not have Parameters
"static": "Hello",
"title": "{% $states.input.title %}",
"name": "{% $customerName %}", // With $customerName declared as a variable
"not-evaluated": "$customerName"
}
}
给定分配给 "María" 的输入 { "title" : "Doctor" } 和变量 customerName,两台状态机都将生成以下 JSON 结果:
{
"static": "Hello",
"title": "Doctor",
"name": "María",
"not-evaluated": "$customerName"
}
在下图中,您可以看到一个图形化表示,其中显示将 JSONPath(左)转换为 JSONata(右)会如何降低状态机中步骤的复杂性:
您可以(可选)从状态输入中选择数据,并将其转换为参数以发送到您的集成操作。使用 JSONata,您便可(可选)选择并转换操作的结果,以分配给变量以及用于状态输出。
注意:分配和输出步骤并行进行。如果您选择在变量赋值期间转换数据,则转换后的数据将不可用于“输出”步骤。您必须在“输出”步骤中重新应用 JSONata 转换。
QueryLanguage 字段
在您的工作流 ASL 定义中,有一个位于状态机定义顶级和各个状态中的 QueryLanguage 字段。通过在各个状态内设置 QueryLanguage,可以在现有状态机中逐步采用 JSONata,而不是一次性对状态机进行全面升级。
QueryLanguage 字段可设置为 "JSONPath" 或 "JSONata"。如果省略顶级 QueryLanguage 字段,则默认为 "JSONPath"。如果状态包含状态级 QueryLanguage 字段,则 Step Functions 将使用该状态的指定查询语言。如果该状态不包含 QueryLanguage 字段,则它将使用顶级 QueryLanguage 字段中指定的查询语言。
用 JSON 字符串编写 JSONata 表达式
当 ASL 字段值、JSON 对象字段值或 JSON 数组元素值中的字符串被 {% %} 字符包裹时,该字符串将被解析为 JSONata。注意,字符串必须以没有前导空格的 {% 开头,并且必须以没有尾随空格的 %} 结尾。表达式打开或关闭不当将会导致验证错误。
一些示例:
-
"TimeoutSeconds" : "{% $timeout %}" -
Task状态下的"Arguments" : {"field1" : "{% $name %}"} -
Map状态下的"Items": [1, "{% $two %}", 3]
并非所有 ASL 字段都接受 JSONata。例如,必须将每个状态的 Type 字段设置为常量字符串。同样,Task 状态的 Resource 字段必须是常量字符串。Map状态Items字段将接受 JSON 数组、JSON 对象或 jsonata 表达式,其计算结果必须为数组或对象。
保留变量:$states
Step Functions 定义了一个名为的保留变量$states。在 JSONata 状态下,将以下结构分配给 $states 以在 JSONata 表达式中使用:
# Reserved $states variable in JSONata states
$states = {
"input": // Original input to the state
"result": // API or sub-workflow's result (if successful)
"errorOutput": // Error Output (only available in a Catch)
"context": // Context object
}
进入状态时,Step Functions 会将状态输入分配给。$states.input$states.input 的值可用于所有接受 JSONata 表达式的字段。$states.input 始终是指原始状态输入。
对于 Task、Parallel 和 Map 状态:
-
$states.result如果成功,则指 API 或子工作流程的原始结果。 -
$states.errorOutput指的是 API 或子工作流程失败时的错误输出。$states.errorOutput可用于Catch字段的Assign或Output。
如果在无法访问 $states.result 或 $states.errorOutput 的字段和状态中尝试对其进行访问,则在创建、更新或验证状态机时将捕获该尝试。
$states.context 对象为您的工作流提供有关其具体执行的信息,例如 StartTime、任务令牌和初始工作流输入。要了解更多信息,请参阅在 Step Functions 中从上下文对象访问执行数据。
处理表达式错误
在运行时,JSONata 表达式求值可能由于多种原因而失败,例如:
-
类型错误 - 如果
$x或$y不是数字,则表达式(例如{% $x + $y %})将失败。 -
类型不兼容 - 表达式的计算结果可能为该字段不接受的类型。例如,字段
TimeoutSeconds需要数字输入,因此如果$timeout返回字符串,则表达式{% $timeout %}将失败。 -
值超出范围 - 生成超出字段可接受范围的值的表达式将失败。例如,诸如
{% $evaluatesToNegativeNumber %}之类的表达式在TimeoutSeconds字段中将会失败。 -
未能返回结果 - JSON 不能表示未定义的值表达式,因此表达式
{% $data.thisFieldDoesNotExist %}会导致错误。 -
超过内存限制-在评估期间消耗过多内存的 JSonata 表达式将失败并出现错误。
Expression evaluation memory limit exceeded处理或转换大量数据的表达式可能会发生这种情况。要解决此限制,请考虑将数据转换移至 Lambda 函数。 -
表达式超时-计算时间超过 1 秒的 jsonata 表达式将因错误而失败。
Expression evaluation timeout包含无限循环或非常昂贵的运算的表达式可能会发生这种情况。 -
堆栈溢出-超过最大递归深度的 jsonata 表达式将失败,并显示为。
Stack overflow error如果递归不是终止的,请确保该函数具有正确的基本情况或终止条件。如果递归终止但调用堆栈变得太深,可以考虑将函数重写为尾递归以减少堆栈深度。
在每种情况下,解释器都会引发错误:States.QueryEvaluationError。您的 Task、Map 和 Parallel 状态可以提供一个 Catch 字段用于捕获错误,并提供一个 Retry 字段用于在出错时重试。
从 JSONPath 转换为 JSONata
以下各节将对使用 JSONPath 和 JSONata 编写的代码进行比较并阐述两者之间的差异。
没有更多路径字段
ASL 要求开发人员在使用 JSONPath 时,使用字段的 Path 版本(如 TimeoutSecondsPath 中那样)从状态数据中选择一个值。使用 JSONata 时,您将不再使用 Path 字段,因为 ASL 会自动在非路径字段(例如 TimeoutSeconds)中为您解释 {% %} 包裹的 JSONata 表达式。
-
JSONPath 旧版示例:
"TimeoutSecondsPath": "$timeout" -
JSONata:
"TimeoutSeconds": "{% $timeout %}"
同样,Map状态ItemsPath已被替换为接受 JSON 数组、JSON 对象或 jsonata 表达式的Items字段,这些表达式必须计算为数组或对象。
JSON 对象
ASL 使用有效载荷模板一词来描述一个 JSON 对象,该对象能够包含用于 Parameters 和 ResultSelector 字段值的 JSONPath 表达式。ASL 将不对 JSONata 使用“有效载荷模板”一词,因为 JSONata 求值针对所有字符串进行,无论它们是单独出现,还是出现在 JSON 对象或 JSON 数组中。
没有更多 .$
ASL 要求您在有效载荷模板中的字段名称后附加“.$”,以使用 JSONPath 和内置函数。指定 "QueryLanguage":"JSONata" 时,您不再对 JSON 对象字段名称使用“.$”约定,而是用 {% %} 字符包裹 JSONata 表达式。无论对象嵌套在其他数组或对象中的深度如何,您都对所有字符串值字段使用相同的约定。
Arguments 和 Output 字段
当设置QueryLanguage为时JSONata,旧的 I/O 处理字段将被禁用(InputPath、ParametersResultSelector、ResultPath和OutputPath),并且大多数州将获得两个新字段:Arguments和Output。
与 jsonPath 中使用的字段相比,JSonata 提供了一种更简单的 I/O 转换方法。JSONata 的功能使得 Arguments 和 Output 比 JSONPath 之前的五个字段更强大。这些新的字段名称还有助于简化 ASL,并阐明值传递和返回模型。
Arguments 和 Output 字段(以及其他类似字段,如 Map 状态的 ItemSelector)将接受 JSON 对象,例如:
"Arguments": {
"field1": 42,
"field2": "{% jsonata expression %}"
}
或者,您也可以直接使用 JSONata 表达式,例如:
"Output": "{% jsonata expression %}"
输出也可以接受任何类型的 JSON 值,例如:"Output":true、"Output":42。
Arguments 和 Output 字段仅支持 JSONata,因此将它们与使用 JSONPath 的工作流结合使用是无效的。相反,JSONPath 仅支持 InputPath、Parameters、ResultSelector、ResultPath、OutputPath 和其他 JSONPath 字段,因此在使用 JSONata 作为顶级工作流或状态查询语言时,使用基于路径的字段是无效的。
Pass 状态
Pass 状态中的可选结果以前被视为虚拟任务的输出。选择 JSONata 作为工作流或状态查询语言后,您现在可以使用新的 Output 字段。
Choice 状态
使用 JSONPath 时,Choice 状态具有输入 Variable 和许多比较路径,例如以下 NumericLessThanEqualsPath:
# JSONPath choice state sample, with Variable and comparison path
"Check Price": {
"Type": "Choice",
"Default": "Pause",
"Choices": [
{
"Variable": "$.current_price.current_price",
"NumericLessThanEqualsPath": "$.desired_price",
"Next": "Send Notification"
} ],
}
使用 JSONata 时,Choice 状态有一个 Condition,您可以在其中使用 JSONata 表达式:
# Choice state after JSONata conversion
"Check Price": {
"Type": "Choice",
"Default": "Pause"
"Choices": [
{
"Condition": "{% $current_price <= $states.input.desired_priced %}",
"Next": "Send Notification"
} ]
注意:变量和比较字段仅适用于 JSONPath。Condition 仅可用于 JSONata。
JSONata 示例
可以在 Workflow Studio 中创建以下示例来试验 JSONata。您可以创建和执行状态机,也可以使用 Test 状态来传入数据,甚至修改状态机定义。
示例:输入和输出
此示例展示了当您选择使用 JSONata 时,如何利用 $states.input 来使用状态输入,以及如何利用 Output 字段来指定状态输出。
{ "Comment": "Input and Output example using JSONata", "QueryLanguage": "JSONata", "StartAt": "Basic Input and Output", "States": { "Basic Input and Output": { "QueryLanguage": "JSONata", "Type": "Succeed", "Output": { "lastName": "{% 'Last=>' & $states.input.customer.lastName %}", "orderValue": "{% $states.input.order.total %}" } } } }
当使用以下内容作为输入来执行工作流时:
{ "customer": { "firstName": "Martha", "lastName": "Rivera" }, "order": { "items": 7, "total": 27.91 } }
Test 状态或状态机执行将返回以下 JSON 输出:
{
"lastName": "Last=>Rivera",
"orderValue": 27.91
}
示例:使用 JSONata 进行筛选
您可以使用 JSONata 路径运算符FilterDietProducts 状态。
使用 JSONata 进行筛选的状态机定义
{ "Comment": "Filter products using JSONata", "QueryLanguage": "JSONata", "StartAt": "FilterDietProducts", "States": { "FilterDietProducts": { "Type": "Pass", "Output": { "dietProducts": "{% $states.input.products[calories=0] %}" }, "End": true } } }
测试的示例输入
{ "products": [ { "calories": 140, "flavour": "Cola", "name": "Product-1" }, { "calories": 0, "flavour": "Cola", "name": "Product-2" }, { "calories": 160, "flavour": "Orange", "name": "Product-3" }, { "calories": 100, "flavour": "Orange", "name": "Product-4" }, { "calories": 0, "flavour": "Lime", "name": "Product-5" } ] }
状态机中步骤测试的输出
{ "dietProducts": [ { "calories": 0, "flavour": "Cola", "name": "Product-2" }, { "calories": 0, "flavour": "Lime", "name": "Product-5" } ] }
示例:在 Map 状态下使用之前的状态输出
此示例说明如何使用 jsonata 使用先前状态的输出作为 Map 状态的输入。以下工作流程使用 “通过” 状态来模拟一个任务,该任务返回带有项目列表的订单,然后 Map 状态从该输出中选择项目数组并对其进行迭代。
状态机定义
{ "Comment": "Example: Using previous state output in a Map state with JSONata", "QueryLanguage": "JSONata", "StartAt": "GetOrder", "States": { "GetOrder": { "Type": "Pass", "Output": { "orderId": "{% $states.input.orderId %}", "items": "{% $states.input.items %}" }, "Next": "ProcessItems" }, "ProcessItems": { "Type": "Map", "Items": "{% $states.input.items %}", "ItemProcessor": { "ProcessorConfig": { "Mode": "INLINE" }, "StartAt": "CalculateItemTotal", "States": { "CalculateItemTotal": { "Type": "Pass", "Output": { "name": "{% $states.input.name %}", "total": "{% $states.input.price * $states.input.quantity %}" }, "End": true } } }, "End": true } } }
在此定义中,GetOrder状态不变地输出订单数据。ProcessItemsMap 状态"Items": "{% $states.input.items %}"用于从的输出中选择items数组GetOrder。每次迭代都会从数组中接收一个项目,并通过将价格乘以数量来计算项目总额。
示例输入
{ "orderId": "ORD-1234", "items": [ { "name": "Widget", "price": 4.99, "quantity": 3 }, { "name": "Gadget", "price": 12.50, "quantity": 2 }, { "name": "Bolt", "price": 0.75, "quantity": 10 } ] }
预期产出
[
{
"name": "Widget",
"total": 14.97
},
{
"name": "Gadget",
"total": 25
},
{
"name": "Bolt",
"total": 7.5
}
]
示例:扁平化并行状态输出
当你使用 Parallel 状态时,它会返回一个数组,其中每个元素都是一个分支的输出。使用 jsonata,您可以使用 “并行” 状态下的Output字段将这些结果展平或合并到单个对象中。这种方法取代了 jsonPath 字段ResultSelector。
以下示例使用具有两个分支的并行状态。每个分支都使用通过状态来模拟 D GetItem ynamoDB 调用。Parallel 状态在其Output字段$merge($states.result)中使用来将分支结果合并到单个对象中。
状态机定义
{ "Comment": "Example: Flattening Parallel state output with JSONata", "QueryLanguage": "JSONata", "StartAt": "GetOrderAndCustomer", "States": { "GetOrderAndCustomer": { "Type": "Parallel", "Output": "{% $merge($states.result) %}", "Branches": [ { "StartAt": "Get Order", "States": { "Get Order": { "Type": "Pass", "Output": { "orderId": "{% $states.input.orderId %}", "orderDate": "2024-11-20", "total": 35.99 }, "End": true } } }, { "StartAt": "Get Customer", "States": { "Get Customer": { "Type": "Pass", "Output": { "customerId": "{% $states.input.customerId %}", "customerName": "Martha Rivera", "email": "martha@example.com" }, "End": true } } } ], "Next": "Done" }, "Done": { "Type": "Succeed" } } }
示例输入
{ "orderId": "12345", "customerId": "C-100" }
预期产出
{
"orderId": "12345",
"orderDate": "2024-11-20",
"total": 35.99,
"customerId": "C-100",
"customerName": "Martha Rivera",
"email": "martha@example.com"
}
该$merge()函数将一个对象数组组合成一个对象。如果分支返回具有重叠键的对象,则以后的数组元素优先。分支结果的排序一致——它们对应于状态机定义中Branches数组的顺序。
Step Functions 提供的 JSONata 函数
JSonata 包含字符串、数字、聚合、布尔函数、数组 Date/Time、对象和高阶函数的函数库。Step Functions 提供了其他可在 JSONata 表达式中使用的 JSONata 函数。这些内置函数可以替换 Step Functions 内置函数。内置函数仅适用于使用 JSONPath 查询语言的状态。
注意:需要整数值作为参数的 Built-in jsonata 函数将自动向下舍入所提供的任何非整数。
$partition - 等效于 States.ArrayPartition 内置函数的 JSONata,用于对大型数组进行分区。
第一个参数是要分区的数组,第二个参数是代表区块大小的整数。返回值将是一个二维数组。解释器将输入数组分成多个数组,其大小由区块大小指定。如果数组中剩余的项目数小于区块大小,则最后一个数组区块的长度可能小于之前的数组区块的长度。
"Assign": {
"arrayPartition": "{% $partition([1,2,3,4], $states.input.chunkSize) %}"
}
$range - 等效于 States.ArrayRange 内置函数的 JSONata,用于生成值数组。
这个函数需要三个参数。第一个参数是代表新数组第一个元素的整数,第二个参数是代表新数组最后一个元素的整数,第三个参数是新数组中元素的增量值整数。返回值是一个新生成的值数组,范围从函数的第一个参数到函数的第二个参数,两者之间的元素根据增量进行调整。增量值可以是正值,也可以是负值,它将从最后一个元素开始依次递增或递减,直至达到或超过最终值。
"Assign": {
"arrayRange": "{% $range(0, 10, 2) %}"
}
$hash - 等效于 States.Hash 内置函数的 JSONata,用于计算给定输入的哈希值。
这个函数需要两个参数。第一个参数是要进行哈希处理的源字符串。第二个参数是代表用于哈希计算的哈希算法的字符串。哈希算法必须是以下值之一:"MD5"、"SHA-1"、"SHA-256"、"SHA-384"、"SHA-512"。返回值是计算得出的数据哈希值的字符串。
之所以创建此函数,是因为 JSONata 本身并不具备计算哈希值的能力。
"Assign": {
"myHash": "{% $hash($states.input.content, $hashAlgorithmName) %}"
}
$random - 等效于 States.MathRandom 内置函数的 JSONata,用于返回随机数 n,其中 0 ≤ n < 1。
该函数接受一个可选的整数参数,表示随机函数的种子值。如果您使用具有相同种子值的函数,它将返回相同的数字。
之所以创建这个重载函数,是因为内置的 JSONata 函数 $random
"Assign": {
"randNoSeed": "{% $random() %}",
"randSeeded": "{% $random($states.input.seed) %}"
}
$uuid - States.UUID 内置函数的 JSONata 版本。
此函数不接受任何参数。此函数会返回一个 v4 UUID。
之所以创建此函数,是因为 JSONata 本身并不具备生成 UUID 的能力。
"Assign": {
"uniqueId": "{% $uuid() %}"
}
$parse - 用于将 JSON 字符串反序列化的 JSONata 函数。
此函数将字符串化的 JSON 作为其唯一参数。
JSONata 通过 $eval 支持此功能;但是,Step Functions 工作流不支持 $eval。
"Assign": {
"deserializedPayload": "{% $parse($states.input.json_string) %}"
}