在 Step Functions 中使用 JSONata 转换数据
使用 JSONata,您可以获得一种强大的开源查询和表达式语言,用于在工作流中选择和转换数据。有关简要介绍和完整的 JSONata 参考,请参阅 JSONata.org 文档
您必须选择加入,以便对现有工作流使用 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 数组或者必须能计算得出数组的 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 %}会导致错误。
在每种情况下,解释器都会引发错误: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 已替换为 Items 字段,后者接受 JSON 数组或必须能计算得出数组的 JSONata 表达式。
JSON 对象
ASL 使用有效载荷模板一词来描述一个 JSON 对象,该对象能够包含用于 Parameters 和 ResultSelector 字段值的 JSONPath 表达式。ASL 将不对 JSONata 使用“有效载荷模板”一词,因为 JSONata 求值针对所有字符串进行,无论它们是单独出现,还是出现在 JSON 对象或 JSON 数组中。
没有更多 .$
ASL 要求您在有效载荷模板中的字段名称后附加“.$”,以使用 JSONPath 和内置函数。指定 "QueryLanguage":"JSONata" 时,您不再对 JSON 对象字段名称使用“.$”约定,而是用 {% %} 字符包裹 JSONata 表达式。无论对象嵌套在其他数组或对象中的深度如何,您都对所有字符串值字段使用相同的约定。
Arguments 和 Output 字段
当 QueryLanguage 设置为 JSONata 时,将禁用旧的 I/O 处理字段(InputPath、Parameters、ResultSelector、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" } ] }
Step Functions 提供的 JSONata 函数
JSONata 包含字符串、数字、聚合、布尔值、数组、对象、日期/时间和高阶函数的函数库。Step Functions 提供了其他可在 JSONata 表达式中使用的 JSONata 函数。这些内置函数可以替换 Step Functions 内置函数。内置函数仅适用于使用 JSONPath 查询语言的状态。
注意:需要整数值作为参数的内置 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) %}"
}