为什么需要 JSON 转换?
在现代开发中,JSON 数据来自各种来源——API 响应、配置文件、数据库、日志。原始数据往往不能直接使用,需要经过转换才能满足业务需求。常见的转换场景包括:
- 从嵌套结构中提取特定字段
- 过滤符合条件的记录
- 将数据从一种结构映射为另一种
- 聚合统计(求和、分组、计数)
- 多数据源合并
jq:命令行 JSON 处理利器
jq 是处理 JSON 的瑞士军刀,支持过滤、映射、转换,语法简洁且功能强大。
基础语法
# 格式化输出
echo '{"name":"张三","age":30}' | jq '.'
# 提取字段
echo '{"name":"张三","age":30}' | jq '.name'
# "张三"
# 提取嵌套字段
echo '{"user":{"name":"张三"}}' | jq '.user.name'
数组操作
# 数组所有元素
echo '[1,2,3,4,5]' | jq '.[]'
# 1 2 3 4 5
# 数组切片
echo '[1,2,3,4,5]' | jq '.[1:3]'
# [2,3]
# map 映射
echo '[{"name":"张三","age":30},{"name":"李四","age":25}]' | jq 'map(.name)'
# ["张三","李四"]
# select 过滤
echo '[{"name":"张三","age":30},{"name":"李四","age":25}]' | jq '.[] | select(.age > 28)'
# {"name":"张三","age":30}
高级转换
# 构造新对象
echo '[{"name":"张三","age":30}]' | jq 'map({username: .name, years: .age})'
# [{"username":"张三","years":30}]
# 分组统计
echo '[{"dept":"技术","salary":10000},{"dept":"技术","salary":12000},{"dept":"市场","salary":8000}]' \
| jq 'group_by(.dept) | map({dept: .[0].dept, total: (map(.salary) | add)})'
# [{"dept":"技术","total":22000},{"dept":"市场","total":8000}]
# 键值转换
echo '{"a":1,"b":2,"c":3}' | jq 'to_entries | map({(.key): (.value * 10)}) | add'
# {"a":10,"b":20,"c":30}
JSONPath 查询语言
JSONPath 是类似 XPath 的 JSON 查询表达式,广泛用于编程库和 API 查询中。
基本语法
| 表达式 | 说明 |
|---|---|
$ |
根对象 |
.property |
访问属性 |
['property'] |
访问属性(带特殊字符时使用) |
[index] |
数组索引 |
[start:end] |
数组切片 |
[*] |
所有元素 |
.. |
递归下降 |
?(filter) |
过滤表达式 |
查询示例
给定数据:
{
"store": {
"books": [
{"title": "深入理解计算机系统", "price": 99, "category": "技术"},
{"title": "代码大全", "price": 108, "category": "技术"},
{"title": "小王子", "price": 25, "category": "文学"}
]
}
}
// 所有书名
$.store.books[*].title
// 价格大于 50 的书
$.store.books[?(@.price > 50)]
// 技术类书籍
$.store.books[?(@.category == '技术')]
// 递归查找所有 price
$..price
各语言实现
- JavaScript:
jsonpath-plus、jsonpath - Python:
jsonpath-ng - Java:
Jayway JsonPath - Go:
github.com/PaesslerAG/jsonpath
映射(Map)
映射是将数据集合中的每个元素转换为新形式的过程。
JavaScript
const users = [
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' }
];
// 提取姓名列表
const names = users.map(u => u.name);
// ["张三", "李四"]
// 转换为以 ID 为键的对象
const userMap = Object.fromEntries(users.map(u => [u.id, u]));
// {1: {...}, 2: {...}}
// 结构重塑
const dtos = users.map(({ id, name }) => ({ userId: id, displayName: name }));
Python
users = [
{"id": 1, "name": "张三", "email": "zhang@example.com"},
{"id": 2, "name": "李四", "email": "li@example.com"}
]
names = [u["name"] for u in users]
user_map = {u["id"]: u for u in users}
过滤(Filter)
过滤是保留满足条件的元素。
const products = [
{ name: '键盘', price: 200, inStock: true },
{ name: '鼠标', price: 80, inStock: false },
{ name: '显示器', price: 1500, inStock: true }
];
// 有库存且价格低于 1000
const affordable = products.filter(p => p.inStock && p.price < 1000);
// [{ name: '键盘', ... }]
归约(Reduce)
归约将集合累积为单个值,用于求和、分组、扁平化等场景。
const orders = [
{ product: '键盘', qty: 2, price: 200 },
{ product: '鼠标', qty: 3, price: 80 },
{ product: '键盘', qty: 1, price: 200 }
];
// 总金额
const total = orders.reduce((sum, o) => sum + o.qty * o.price, 0);
// 1040
// 按商品分组统计数量
const grouped = orders.reduce((acc, o) => {
acc[o.product] = (acc[o.product] || 0) + o.qty;
return acc;
}, {});
// { 键盘: 3, 鼠标: 3 }
// 扁平化嵌套数组
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5]
实战案例:API 数据转换
场景:将分页 API 响应转换为前端表格数据
后端返回:
{
"code": 200,
"data": {
"total": 100,
"page": 1,
"list": [
{"user_id": 1, "user_name": "张三", "created_at": "2025-07-01T10:00:00Z"},
{"user_id": 2, "user_name": "李四", "created_at": "2025-07-02T11:30:00Z"}
]
}
}
转换为目标格式:
function transformResponse(resp) {
return {
total: resp.data.total,
currentPage: resp.data.page,
rows: resp.data.list.map(item => ({
id: item.user_id,
name: item.user_name,
date: new Date(item.created_at).toLocaleDateString('zh-CN')
}))
};
}
场景:树形结构扁平化
function flattenTree(nodes, parentId = null) {
return nodes.reduce((acc, node) => {
const flatNode = { id: node.id, name: node.name, parentId };
acc.push(flatNode);
if (node.children) {
acc.push(...flattenTree(node.children, node.id));
}
return acc;
}, []);
}
性能建议
- 处理大型 JSON(>10MB)时,优先使用流式解析,避免一次性加载到内存
jq处理大文件时加--slurp或使用流模式--stream- 前端深层遍历注意时间复杂度,避免在渲染循环中做复杂转换
- 对于固定结构的转换,考虑使用 JSON Schema + 代码生成工具
工具推荐
| 工具 | 类型 | 适用场景 |
|---|---|---|
| jq | 命令行 | Shell 脚本、快速调试 |
| JSONPath Plus | JS 库 | 复杂查询表达式 |
| Jayway JsonPath | Java 库 | Spring 项目集成 |
| JMESPath | 查询语言 | AWS CLI、通用查询 |
| jsonata | 表达式语言 | 复杂转换规则 |