在 Step Functions 中使用 JSONata 转换数据 - Amazon Step Functions
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

在 Step Functions 中使用 JSONata 转换数据

使用 JSONata,您可以获得一种强大的开源查询和表达式语言,用于在工作流中选择转换数据。有关简要介绍和完整的 JSONata 参考,请参阅 JSONata.org 文档

您必须选择加入,以便对现有工作流使用 JSONata 查询和转换语言。在控制台中创建工作流时,建议为顶级状态机 QueryLanguage 选择 JSONata。对于使用 JSONPath 的现有工作流或新工作流,控制台提供了将各个状态转换为 JSONata 的选项。

选择 JSONata 后,您的工作流字段将从五个 JSONPath 字段(InputPathParametersResultSelectorResultPathOutputPath)减少到只有两个字段:ArgumentsOutput。此外,您不会在 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(右)会如何降低状态机中步骤的复杂性:

该图比较了 JSONPath 和 JSONata 状态中的字段情况。

您可以(可选)从状态输入中选择数据,并将其转换为参数以发送到您的集成操作。使用 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 始终是指原始状态输入。

对于 TaskParallelMap 状态:

  • $states.result 指 API 或子工作流成功时的原始结果。

  • $states.errorOutput 指 API 或子工作流失败时的错误输出。

    $states.errorOutput 可用于 Catch 字段的 AssignOutput

如果在无法访问 $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 对象,该对象能够包含用于 ParametersResultSelector 字段值的 JSONPath 表达式。ASL 将不对 JSONata 使用“有效载荷模板”一词,因为 JSONata 求值针对所有字符串进行,无论它们是单独出现,还是出现在 JSON 对象或 JSON 数组中。

没有更多 .$

ASL 要求您在有效载荷模板中的字段名称后附加“.$”,以使用 JSONPath 和内置函数。指定 "QueryLanguage":"JSONata" 时,您不再对 JSON 对象字段名称使用“.$”约定,而是用 {% %} 字符包裹 JSONata 表达式。无论对象嵌套在其他数组或对象中的深度如何,您都对所有字符串值字段使用相同的约定。

Arguments 和 Output 字段

QueryLanguage 设置为 JSONata 时,将禁用旧的 I/O 处理字段(InputPathParametersResultSelectorResultPathOutputPath),并且大多数状态将获得两个新字段:ArgumentsOutput

与 JSONPath 中使用的字段相比,JSONata 提供了一种更简单的方法来执行 I/O 转换。JSONata 的功能使得 ArgumentsOutput 比 JSONPath 之前的五个字段更强大。这些新的字段名称还有助于简化 ASL,并阐明值传递和返回模型。

ArgumentsOutput 字段(以及其他类似字段,如 Map 状态的 ItemSelector)将接受 JSON 对象,例如:

"Arguments": { "field1": 42, "field2": "{% jsonata expression %}" }

或者,您也可以直接使用 JSONata 表达式,例如:

"Output": "{% jsonata expression %}"

输出也可以接受任何类型的 JSON 值,例如:"Output":true"Output":42

ArgumentsOutput 字段仅支持 JSONata,因此将它们与使用 JSONPath 的工作流结合使用是无效的。相反,JSONPath 仅支持 InputPathParametersResultSelectorResultPathOutputPath 和其他 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 路径运算符筛选数据。例如,假设您有一份可供输入的产品清单,而您只想处理卡路里含量为零的产品。您可以使用以下 ASL 创建状态机定义,并使用随后的示例输入来测试 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" } ] }
被测 JSONata 表达式的示例输出。

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) %}" }