jsonata是ibm的open source项目,正是这样做的。它是一种用于查询和转换 json 数据结构的表达式语言。它简单而优雅。json 查询和转换语言

介绍

此语言的主要用途是从 json 文档中提取值,并具有使用一组基本函数和运算符组合这些值的附加功能,以及将输出格式化为任意 json 结构的功能。

基本选择

为了支持从 json 结构中提取值,定义了位置路径语法。与 xpath 一样,这将选择文档中与指定位置路径匹配的所有可能值。json 的两个结构构造是对象和数组。

导航 json 对象

json 对象是一个关联数组(也称为映射或哈希)。导航到任意深度嵌套结构的 json 对象的位置路径语法由点 '.' 分隔符分隔的字段名称组成。该表达式返回导航到位置路径中的最后一步后引用的 json 值。如果在位置路径的导航过程中找不到字段,则表达式不返回任何内容(由 javascript undefined 表示)。不会因输入文档中不存在的数据而引发任何错误。

除非另有说明,否则本指南中的示例将使用以下示例 json 文档:

{
  "firstname": "fred",
  "surname": "smith",
  "age": 28,
  "address": {
    "street": "hursley park",
    "city": "winchester",
    "postcode": "so21 2jn"
  },
  "phone": [
    {
      "type": "home",
      "number": "0203 544 1234"
    },
    {
      "type": "office",
      "number": "01962 001234"
    },
    {
      "type": "office",
      "number": "01962 001235"
    },
    {
      "type": "mobile",
      "number": "077 7700 1234"
    }
  ],
  "email": [
    {
      "type": "work",
      "address": ["fred.smith@my-work.com", "fsmith@my-work.com"]
    },
    {
      "type": "home",
      "address": ["freddy@my-social.com", "frederic.smith@very-serious.com"]
    }
  ],
  "other": {
    "over 18 ?": true,
    "misc": null,
    "alternative.address": {
      "street": "brick lane",
      "city": "london",
      "postcode": "e1 6rf"
    }
  }
}

以下表达式在应用于此 json 文档时将生成以下结果:

表达输出评论
surname"smith"返回 json 字符串("双引号")
age28返回一个 json 数字
address.city"winchester"字段引用,以 "."分隔
other.miscnull匹配路径并返回空值
other.nothing
找不到路径。返回未定义的 javascript
other.'over 18 ?'true包含空格或保留标记的
字段引用可以括在引号中(单引号或双引号)

导航 json 数组

当需要有序的值集合时,将使用 json 数组。
数组中的每个值都与索引(位置)而不是名称相关联,因此为了处理数组中的各个值,需要额外的语法来指定索引。这是在数组的字段名称后使用方括号完成的。如果方括号包含数字或计算结果为数字的表达式,则该数字表示要选择的值的索引。索引是零偏移量,即数组中的第一个值是 。如果数字不是整数,则将其向下舍入为整数。如果方括号中的表达式是非数字的,或者是计算结果不为数字的表达式,则将其视为谓词。arrarr[0]

例如,从数组末尾开始计数的负索引将选择最后一个值、倒数第二个值等。如果指定的索引超过数组的大小,则不会选择任何内容。arr[-1]arr[-2]

如果没有为数组指定索引(即字段引用后没有方括号),则选择整个数组。如果数组包含对象,并且位置路径选择这些对象中的字段,则数组中的每个对象都将被查询以进行选择。

表达输出评论
phone[0]{ "type": "home", "number": "0203 544 1234" }返回第一项(一个对象)
phone[1]{ "type": "office", "number": "01962 001234" }返回第二项
phone[-1]{ "type": "mobile", "number": "077 7700 1234" }返回最后一项
phone[-2]{ "type": "office", "number": "01962 001235" }从末尾开始的负索引计数
phone[8]
不存在 - 不返回任何内容
phone[0].number"0203 544 1234"选择第一项中的字段number
phone.number[ "0203 544 1234", "01962 001234", "01962 001235", "077 7700 1234" ]没有给出索引,因此它选择所有
字段(整个数组),然后为每个字段选择所有字段phonenumber
phone.number[0][ "0203 544 1234", "01962 001234", "01962 001235", "077 7700 1234" ]可能期望它只返回第一个数字,
但它返回每个项目的第一个数字phone
(phone.number)[0]"0203 544 1234"将索引应用于 返回的数组。括号的一次使用。phone.number
顶级数组、嵌套数组和数组扁平化

考虑 json 文档:

[
  { "ref": [ 1,2 ] },
  { "ref": [ 3,4 ] }
]

在顶层,我们有一个数组而不是一个对象。如果我们要选择此顶级数组中的第一个对象,则没有要追加到的字段名称。我们不能单独使用,因为这与数组构造函数语法冲突。但是,我们可以使用上下文引用来引用文档的开头,如下所示:[0][0]$

表达输出评论
$[0]{ "ref": [ 1,2 ] }$表达式开头引用整个输入文档
$[0].ref[ 1,2 ].ref这里返回整个内部数组
$[0].ref[0]1返回内部数组第一个位置上的元素
$.ref[ 1, 2, 3, 4 ]尽管嵌套数组的结构不同,但生成的选择
被平展为单个平面数组。输入数组的原始嵌套结构
将丢失。有关如何在结果中
维护原始结构的信息,请参阅数组构造函数。

复杂选择

通配符

使用字段名称代替字段来选择对象中的所有字段*

表达输出评论
address.*[ "hursley park", "winchester", "so21 2jn" ]选择address
*.postcode"so21 2jn"选择任何子对象的值postcode

导航任意深度

后代通配符将遍历所有后代(多级通配符)。***

表达输出评论
**.postcode[ "so21 2jn", "e1 6rf" ]选择所有值,无论它们在结构中的嵌套深度如何postcode

谓词

在位置路径的任何步骤中,都可以使用谓词 - [expr] 筛选所选项目,其中 expr 的计算结果为布尔值。选择中的每个项目都针对表达式进行测试,如果它的计算结果为 true,则保留该项目;如果为 false,则将其从所选内容中删除。表达式是相对于正在测试的当前(上下文)项计算的,因此,如果谓词表达式执行导航,则它是相对于此上下文项计算的。

例子:

表达输出评论
phone[type='mobile']{ "type": "mobile", "number": "077 7700 1234" }选择字段等于 的项目。phonetype"mobile"
phone[type='mobile'].number"077 7700 1234"选择手机号码
phone[type='office'].number[ "01962 001234", "01962 001235" ]选择办公室电话号码 - 其中有两个!

单例数组和值等效性

在 jsonata 表达式或子表达式中,任何值(其本身不是数组)和仅包含该值的数组都被视为等效的。这允许语言是可组合的,以便从中提取单个值的位置路径以及从数组中提取多个值的对象和位置路径都可以用作其他表达式的输入,而无需为这两种形式使用不同的语法。

请考虑以下示例:

  • address.city返回单个值"winchester"

  • phone[0].number匹配单个值,并返回该值"0203 544 1234"

  • phone[type='home'].number同样匹配单个值"0203 544 1234"

  • phone[type='office'].number匹配两个值,因此返回一个数组[ "01962 001234", "01962 001235" ]

在处理 jsonata 表达式的返回值时,无论匹配了多少个值,都可能需要以一致的格式显示结果。在上面的前两个表达式中,很明显,每个表达式都在处理结构中的单个值,并且只返回该值是有意义的。但是,在最后两个表达式中,匹配的值并不明显,如果宿主语言必须根据返回的内容以不同的方式处理结果,则没有帮助。

如果这是一个问题,则可以修改表达式以使其返回数组,即使只匹配单个值。这是通过向位置路径中的步骤添加空方括号来完成的。可以重写上面的示例以始终返回数组,如下所示:[]

  • address[].city返回[ "winchester"]

  • phone[0][].number返回[ "0203 544 1234" ]

  • phone[][type='home'].number返回[ "0203 544 1234" ]

  • phone[type='office'].number[]返回[ "01962 001234", "01962 001235" ]

请注意,可以放置在谓词的任一侧以及路径表达式中的任何步骤上[]

组合值

字符串表达式

指向字符串值的路径表达式将返回该值。可以使用串联运算符"&"组合字符串

例子

表达输出评论
firstname & ' ' & surname"fred smith"连接后跟空格
,后跟firstnamesurname
address.(street & ', ' & city)"hursley park, winchester"括号的另一个很好的用法

请考虑以下 json 文档:

{
  "numbers": [1, 2.4, 3.5, 10, 20.9, 30]
}

数值表达式

指向数字值的路径表达式将返回该值。可以使用通常的数学运算符将数字组合在一起以生成结果数字。支持的运算符:

  • 加法

  • -减法

  • *乘法

  • /划分

  • %余数(模)

例子

表达输出评论
numbers[0] numbers[1]3.4添加 2 个价格
numbers[0] - numbers[4]-19.9减法
numbers[0] * numbers[5]30将价格乘以数量
numbers[0] / numbers[4]0.04784688995215划分
numbers[2] % numbers[5]3.5模运算符

比较表达式

通常用于谓词中,用于比较两个值。返回布尔值 true 或 false 支持的运算符:

  • =等于

  • !=不等于

  • <小于

  • <=小于或等于

  • >大于

  • >=大于或等于

  • in值包含在数组中

例子

表达输出评论
numbers[0] = numbers[5]平等
numbers[0] != numbers[4]不等式
numbers[1] < numbers[5]小于
numbers[1] <= numbers[5]小于或等于
numbers[2] > numbers[4]大于
numbers[2] >= numbers[4]大于或等于
"01962 001234" in phone.number值包含在

布尔表达式

用于组合布尔结果,通常用于支持更复杂的谓词表达式。支持的运算符:

  • and

  • or

请注意,它作为函数而不是运算符受支持。not

例子

表达输出评论
(numbers[2] != 0) and (numbers[5] != numbers[1])and算子
(numbers[2] != 0) or (numbers[5] = numbers[1])or算子

指定结果结构

到目前为止,我们已经发现了如何从json文档中提取值,以及如何使用数字,字符串和其他运算符操作数据。能够指定此处理后的数据在输出中的显示方式非常有用。

数组构造函数

如前所述,当位置路径与输入文档中的多个值匹配时,这些值将作为数组返回。这些值可能是对象或数组,因此将具有自己的结构,但匹配的值本身位于生成的数组中的顶层。

通过指定位置路径表达式中数组(或对象)的构造,可以在生成的数组中构建额外的结构。在需要字段引用的位置路径中的任何点,都可以插入一对方括号,以指定这些括号内的表达式结果应包含在输出的新数组中。逗号用于分隔数组构造函数中的多个表达式。[]

数组构造函数也可以在位置路径中使用,以便在没有广泛使用通配符的情况下进行多次选择。

例子:

表达输出评论
email.address[ "fred.smith@my-work.com",
"fsmith@my-work.com",
"freddy@my-social.com",
"frederic.smith@very-serious.com" ]
四个电子邮件地址以平面数组形式返回
email.[address][ [ "fred.smith@my-work.com", "fsmith@my-work.com" ],
[ "freddy@my-social.com", "frederic.smith@very-serious.com" ] ]
每个电子邮件对象都会生成一个地址数组
[address, other.'alternative.address'].city[ "winchester", "london" ]selects the value of both
and objectscityaddressalternative.address

object constructors

in a similar manner to the way arrays can be constructed, json objects can also be constructed in the output. at any point in a location path where a field reference is expected, a pair of braces containing key/value pairs separated by commas, with each key and value separated by a colon: . the keys and values can either be literals or can be expressions. the key must either be a string or an expression that evaluates to a string.{}{key1: value2, key2:value2}

when an object constructor follows an expression that selects multiple values, the object constructor will create a single object that contains a key/value pair for each of those context values. if an array of objects is required (one for each context value), then the object constructor should immediately follow the dot '.' operator.

examples:

expressionoutputcomments
phone{type: number}{ "home": "0203 544 1234", "office": "01962 001235", "mobile": "077 7700 1234" }one of the numbers was lost because it had a duplicate keyoffice
phone.{type: number}[ { "home": "0203 544 1234" }, { "office": "01962 001234" }, { "office": "01962 001235" }, { "mobile": "077 7700 1234" } ]produces an array of objects

json literals

the array and object constructors use the standard json syntax for json arrays and json objects. in addition to this values of the other json data types can be entered into an expression using their native json syntax:

  • strings - "hello world"

  • numbers - 34.5

  • booleans - or truefalse

  • nulls - null

  • objects - {"key1": "value1", "key2": "value2"}

  • arrays - ["value1", "value2"]

this means that any valid json document is also a valid expression. this property allows you to use a json document as a template for the desired output, and then replace parts of it with expressions to insert data into the output from the input document.

programming constructs

到目前为止,我们已经介绍了该语言的所有部分,这些部分允许我们从输入 json 文档中提取数据,使用字符串和数字运算符组合数据,以及设置输出 json 文档的结构格式。以下是将其转换为图灵完备的函数式编程语言的部分。

条件表达式

if/then/else 构造可以使用三元运算符 "? :"编写。predicate ? expr1 : expr2

将计算表达式。如果其有效布尔值(参见定义)然后被计算并返回,否则被计算并返回。predicatetrueexpr1expr2

带括号的表达式和块

用于覆盖运算符优先级规则。例如

  • (5 3) * 4

用于计算上下文值上的复杂表达式

  • product.(price * quantity)- "价格"和"数量"都是"产品"值的字段

用于支持"代码块" - 多个表达式,用分号分隔

(expr1; expr2; expr3)

块中的每个表达式都按顺序计算;最后一个表达式的结果从块返回。

变量

任何以美元"$"开头的名称都是一个变量。变量是对值的命名引用。该值可以是语言类型系统(链接)中的任何类型的值之一。

内置变量
  • $没有名称的变量引用输入 json 层次结构中任何点的上下文值。例子

  • $$输入 json 的根。仅当您需要脱离当前上下文以暂时沿着不同的路径导航时才需要。例如,用于交叉引用或联接数据。例子

  • 本机(内置)函数。请参阅函数库。

变量赋值

值(类型系统中的任何类型的值)可以分配给变量

$var_name := "value"

存储的值以后可以使用表达式 引用。$var_name

变量的作用域仅限于分配它的"块"。例如

invoice.(
  $p := product.price;
  $q := product.quantity;
  $p * $q
)

返回发票中产品的价格乘以数量。

功能

该函数是一等类型,可以像任何其他数据类型一样存储在变量中。提供内置函数库(链接),并将其分配给全局作用域中的变量。例如,包含一个函数,当使用字符串参数调用时,该函数将返回一个字符串,其中所有字符都更改为大写。$uppercasestrstr

调用函数

函数的调用方式是在其引用(或定义)之后加上包含逗号分隔的参数序列的括号。例子:

  • $uppercase("hello")返回字符串"hello"。

  • $substring("hello world", 0, 5)返回字符串"hello"

  • $sum([1,2,3])返回数字 6

定义函数

可以使用以下语法定义匿名 (lambda) 函数:

function($l, $w, $h){ $l * $w * $h }

并且可以使用以下命令调用

function($l, $w, $h){ $l * $w * $h }(10, 10, 5)返回 500

该函数也可以分配给变量以供将来使用(在块内)

(
  $volume := function($l, $w, $h){ $l * $w * $h };
  $volume(10, 10, 5);
)
递归函数

已分配给变量的函数可以使用该变量引用调用自身。这允许定义递归函数。例如。

(
  $factorial:= function($x){ $x <= 1 ? 1 : $x * $factorial($x-1) };
  $factorial(4)
)

请注意,实际上可以使用纯匿名函数编写递归函数(即,不会将任何内容分配给变量)。这是使用y组合器完成的,对于那些对函数式编程感兴趣的人来说,这可能是一个有趣的转移。

高阶函数

函数作为一等数据类型,可以作为参数传递给另一个函数,也可以从函数返回。处理其他函数的函数称为高阶函数。请考虑以下示例:

(
  $twice := function($f) { function($x){ $f($f($x)) } };
  $add3 := function($y){ $y   3 };
  $add6 := $twice($add3);
  $add6(7)
)
  • 存储在变量中的函数是高阶函数。它采用一个作为函数的参数,并返回一个函数,该函数采用一个参数,该参数在调用时将该函数应用于 两次。$twice$f$x$f$x

  • $add3存储一个向其参数添加 3 的函数。两者都不是或尚未被调用。$twice$add3

  • $twice通过将函数作为其参数传递来调用。这将返回一个函数,该函数对其参数应用两次。此返回的函数尚未调用,而是分配给变量 。add3$add3add6

  • 最后,使用参数 7 调用 in 中的函数,导致 3 被添加到其中两次。它返回 13。$add6

函数是闭包

定义 lambda 函数时,评估引擎会获取环境的快照,并将其与函数体定义一起存储。环境包括上下文项(即位置路径中的当前值)以及当前范围内的变量绑定。稍后调用 lambda 函数时,将在调用时在该存储环境中而不是在当前环境中执行此操作。此属性称为词法范围,闭包的基本属性。

请考虑以下示例:

account.(
  $accname := function() { $.'account name' };
  order[orderid = 'order104'].product.{
    'account': $accname(),
    'sku-' & $string(productid): $.'product name'
  }
)

创建函数时,上下文项(由 "$"表示)的值为 。稍后,当调用该函数时,上下文项已将结构向下移动到每个项的值。但是,函数体是在定义时存储的环境中调用的,因此其上下文项的值为 。这是一个有点人为的例子,你真的不需要一个函数来做到这一点。该表达式生成以下结果:accountproductaccount

{
  "account": "firefly",
  "sku-858383": "bowler hat",
  "sku-345664": "cloak"
}
高级的东西

无需阅读本节 - 它不会对您的理智或操作json数据的能力有任何帮助。

早些时候,我们学会了如何编写递归函数来计算数字的阶乘,并暗示可以在不命名任何函数的情况下完成此操作。我们可以将高阶函数发挥到极致,并编写以下内容:

λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})}(λ($f) { λ($n) { $n < 2 ? 1 : $n * $f($n - 1) } })(6)

这将产生结果。可以使用希腊语lambda(λ)符号代替单词,如果您可以在键盘上找到它,将节省屏幕空间并取悦lambda演算的粉丝。720function

上述表达式的第一部分是用这种语言实现的 y 组合器。我们可以将其赋值给一个变量,并将其应用于其他递归匿名函数:

(
  $y := λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})};
  [1,2,3,4,5,6,7,8,9] . $y(λ($f) { λ($n) { $n <= 1 ? $n : $f($n-1)   $f($n-2) } }) ($)
)

产生斐波那契级数。[ 1, 1, 2, 3, 5, 8, 13, 21, 34 ]

但我们不需要做任何这些。使用命名函数更明智:

(
  $fib := λ($n) { $n <= 1 ? $n : $fib($n-1)   $fib($n-2) };
  [1,2,3,4,5,6,7,8,9] . $fib($)
)

https://github.com/jsonata-js/jsonata