JSON 工具箱

JMESPath 入门:比 JSONPath 更适合的投影查询

结论:如果目标是把一批对象重排成另一种形状,JMESPath 比 JSONPath 更直接。本文从基础语法讲到投影、管道、内置函数与 AWS CLI 实战。

入门约 9 分钟发布于 2026-10-31更新于 2026-10-31
JMESPath 入门封面

结论:只按位置定位用 JSON Pointer,要筛选或递归用 JSONPath,要把结果重排成另一种形状或在命令行做投影就用 JMESPath;只读一个确定字段两个都不需要。

如果你的目标是「把一批对象重排成另一种形状」,JMESPath 往往比 JSONPath 更直接——它不是去找节点,而是去构造结果。

一、JMESPath 是什么

JMESPath 是一种专门为 JSON 设计的查询语言,最初为 AWS CLI 的输出过滤而设计,后来被 jmespath.js、jmespath.py、go-jmespath 等实现带到各种语言里。它和 JSONPath 的目标不同:

  • JSONPath 的核心是「按路径和条件选出一批节点」,返回的是节点集;
  • JMESPath 的核心是「对输入 JSON 做变换」,返回的是任意形状的新值。

这个定位差异决定了 JMESPath 特别适合做列表投影、字段重命名、结果裁剪,例如把接口返回的 20 个字段裁成前端需要的 3 个字段。如果你还没有读过 JSONPath 的基础语法,可以先看 JSONPath 查询语言实战。

二、基础语法:从标识符到索引

JMESPath 的语法比 JSONPath 少一些符号,读起来更像「提取表达式」:

语法 含义 示例
name 取对象成员 people
a.b 逐层取子成员 store.bicycle.color
[0] 取数组下标 people[0]
[-1] 取最后一个元素 people[-1]
[0:2] 切片,含头不含尾 people[0:2]
[*] 列表投影 people[*].name
[] 展平投影 people[].tags[]
[?condition] 过滤投影 people[?age > 18]
{key: expr} 多选哈希 {name: name, age: age}
[expr, expr] 多选列表 [name, age]
| 管道,把左侧结果交给右侧 people[*].age | [0]

一个最小的例子:

{
  "people": [
    { "name": "Alice", "age": 31, "city": "Shanghai" },
    { "name": "Bob", "age": 17, "city": "Beijing" }
  ]
}
people[0].name      → "Alice"
people[-1].city     → "Beijing"
people[0:2].name    → ["Alice", "Bob"]

需要注意,people[0:2].name 之所以返回数组,是因为切片返回的是列表,后续的 .name 会自动对列表里的每个元素做投影。这是 JMESPath 和普通对象取值的最大区别。

三、投影:把列表变成另一种列表

投影(projection)是 JMESPath 的核心概念。它有两步:先对列表的每个元素求值,再把结果收集成新列表;求值结果为 null 的元素会被自动丢弃。

people[*].name                 // ["Alice", "Bob"]
people[*].{name: name, age: age} // 对象列表
people[?age > 18].name         // ["Alice"]

第二行是一个典型的投影写法:people[*].{name: name, age: age}。它把每个元素重组成 { "name": ..., "age": ... },多出来的 city 被丢掉。这比 JSONPath 拿到节点后再在代码里映射要少一层循环。

两种投影值得区分:

  • [*] 保留原列表结构,空值被跳过;
  • [] 是展平投影,会先把嵌套列表摊平一层,适合 people[].tags[] 这种「取所有人的所有标签」。

四、管道与多选

管道 | 用来「停下当前投影,把结果当成新的输入」。它最常见的用途是投影之后再取下标:

people[*].name | [0]        → "Alice"
people[*].age | sort(@) | [-1] → 最大年龄

如果不写管道,people[*].name[0] 会被理解为对每个 name 取第 0 个字符,结果完全不是你要的。管道是 JMESPath 里最容易被忽略、也最容易救命的符号。

多选有两种:

// 多选哈希:结果是一个对象
people[0].{name: name, age: age}

// 多选列表:结果是一个数组
people[0].[name, age]

多选哈希适合构造具名结果,多选列表适合按位置消费。两者都可以嵌套在投影里,但要注意:多选哈希里如果某个键的值为 null,该键仍然保留;投影则会把整个 null 元素丢掉。

五、内置函数

JMESPath 标准定义了一组函数,常见的有:

函数 作用 示例
length() 取长度 length(people)
sort() 排序 sort(people[*].age)
sort_by() 按字段排序 sort_by(people, &age)
max() / min() 最大值/最小值 max(people[*].age)
sum() 求和 sum(people[*].age)
keys() 取对象键 keys(people[0])
contains() 包含判断 contains(name, "Ali")
to_string() 转字符串 to_string(age)

sort_by 的第二个参数要用 & 表达式,表示「对每个元素取这个字段」:

sort_by(people, &age)[*].name

这套函数让 JMESPath 能完成简单的聚合,而 JSONPath 的定位是「只做选择」,求和、分组通常要换 jq 或代码。

六、与 JSONPath 的对照

维度 JSONPath JMESPath
主要目标 选节点 变换数据
根符号 $ 无,直接写字段名
当前节点 @ @(仅函数参数中)
过滤 [?(@.age > 18)] [?age > 18]
通配 [*] [*] 或 []
构造新对象 不支持 {name: name}
管道 不支持 |
标准 RFC 9535 JMESPath Specification

一句话选择:

  • 你只需要「找出符合条件的节点」,用 JSONPath;
  • 你需要「把结果重排成前端要的形状」,用 JMESPath;
  • 你需要「读一个确定的字段」,两个都不需要,直接取属性。

如果查询结果还要写回文档,这两套语言都不适合,应该改用 JSON Pointer 与补丁,参见 JSON Pointer、Patch 与 Merge Patch。

七、AWS CLI 里的实战

AWS CLI 的 --query 参数就是 JMESPath,这是它最广泛的使用场景:

aws ec2 describe-instances \
  --query 'Reservations[].Instances[].{Id: InstanceId, State: State.Name, Type: InstanceType}' \
  --output table

这条命令做了三件事:用 [] 展平两层嵌套列表,用多选哈希只保留三个字段,再用 --output table 渲染成表格。换成 JSONPath 要写更多层,而且拿不到「重命名 + 裁剪」的效果。

常见写法还有:

# 只看运行中的实例
aws ec2 describe-instances --query 'Reservations[].Instances[?State.Name==`running`].[InstanceId,PublicIpAddress]'

# 取标签里 Name 的值
aws ec2 describe-instances --query 'Reservations[].Instances[].Tags[?Key==`Name`].Value[]'

注意反引号包起来的是字面量,不是字符串。AWS CLI 里字符串要用单引号,JSON 字面量用反引号,这个差异是新手最常见的错误之一。

八、常见错误

  • 把 [*] 和 [] 混用:前者保留嵌套,后者展平一层,结果结构完全不同;
  • 忘记管道:people[*].name[0] 取的是每个名字的首字符,不是第一个名字;
  • 在过滤器里写 @:JMESPath 的过滤条件直接用字段名,[?@.age > 18] 是错的;
  • 把 null 当成空字符串:投影会丢弃 null,但多选哈希会保留键;
  • 在 AWS CLI 里用双引号包字面量:shell 会先展开,导致表达式被改写。

调试建议:先用最小输入验证表达式,再套到真实数据;AWS CLI 可以加 --output json 看原始结果。日常编辑与格式化 JSON 时,配合 JSON 编辑器与命令行工具 能更快定位字段路径。

常见问题

JMESPath 和 JSONPath 可以一起用吗?

可以,但通常没必要。常见做法是先用 JMESPath 裁剪出小对象,再在代码里用普通属性访问;如果确实需要按路径定位再修改,JSONPath 负责查、JSON Pointer 负责改,分工更清晰。

为什么 people[*].name 返回的不是单个字符串?

因为 [*] 是投影,结果永远是一个列表,即使只有一个人。想取单个值要加管道和下标:people[*].name | [0]。

投影里遇到 null 会怎样?

列表投影会自动跳过求值为 null 的元素,所以结果长度可能小于原列表。多选哈希不会丢掉键,而是保留 null 值,这一点在构造 API 响应时要特别注意。

JMESPath 能做聚合和分组吗?

能做基础聚合,例如 sum()、max()、sort_by();但分组、连接、窗口函数不在标准能力范围内。复杂分析还是交给 jq、SQL 或代码。

如何决定用 JMESPath 还是 JSONPath?

需要按路径和条件选出一批节点、拿到节点集时用 JSONPath;需要把结果重排成前端要的形状、做字段重命名与裁剪时用 JMESPath。只读一个确定字段时两个都不需要,直接取属性即可。

是否应该用管道取投影后的第一个值?

应该。people[*].name 是投影,结果永远是列表;不写管道时 people[*].name[0] 会被理解为对每个名字取第 0 个字符。取第一个值要写成 people[*].name | [0]。