JSON数据处理实战指南:格式化、验证与转换
有一次我接到一个导出文件,一个 200 多 MB 的 .json,里面是某个系统几年的订单流水。我图省事,JSON.parse 一把梭——然后浏览器标签页直接卡死,转了十几秒风扇狂转,最后弹出「页面无响应」。那天我才真正意识到:处理 JSON「数据」和格式化一段接口返回,完全是两码事。格式化解决的是「好不好看」,数据处理要解决的是「跑不跑得动」。
后来这类活儿越接越多:有的是嵌套深到第七八层的 API 响应,得把它拍平成一张表;有的是几十万行的 NDJSON,只能一行一行流式读;还有的纯粹是想从一坨结构里把某个字段挖出来,手撸递归写到第三层就开始怀疑人生,最后老老实实上了 JSONPath。这篇就把我这几年在「大、深、杂」的 JSON 上踩过的坑、用顺手的几招记下来——不讲怎么把花括号对齐,只讲数据量上来之后怎么活下去。
三种姿势怎么选:全量 parse / 流式 / JSONPath
先把结论摆桌上。处理 JSON 数据基本就这三种姿势,选错了要么卡死要么白写代码,我按吃过的亏整理成一张表:
| 姿势 | 适用数据量 | 典型场景 | 踩坑点 |
|---|---|---|---|
全量 JSON.parse | 几 MB 以内 | 普通接口返回、配置文件 | 文件一大就卡死主线程,内存还要翻好几倍 |
| 流式 / 逐行解析 | 几十 MB ~ GB 级 | 日志、NDJSON、大导出文件 | 只适合「一行一个对象」的结构,单个巨型对象流式不了 |
| JSONPath 查询 | 不看大小,看结构深 | 从深层嵌套里挖少数几个字段 | 表达式写错容易静默返回空,调试得一段段试 |
我的经验法则:能 JSON.parse 就别整花活;一旦文件超过几十 MB,或者你听到电脑风扇响,就该换流式;而只要嵌套超过四五层、还要反复取值,别犹豫,上 JSONPath。下面挨个展开。
目录
JSON基础回顾
什么是JSON?
JSON是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。
{
"name": "张三",
"age": 30,
"skills": ["JavaScript", "Python", "Go"],
"address": {
"city": "北京",
"district": "朝阳区"
},
"isActive": true,
"salary": null
}
JSON vs JavaScript对象
虽然语法相似,但有重要区别:
| 特性 | JSON | JavaScript对象 |
|---|---|---|
| 键名 | 必须用双引号 | 可以不用引号 |
| 字符串 | 只能用双引号 | 单双引号都可以 |
| 值类型 | 6种(字符串、数字、布尔、null、数组、对象) | 任意类型(函数、Date等) |
| 注释 | 不支持 | 支持 |
| 末尾逗号 | 不允许 | 允许 |
// ❌ 无效的JSON
{
name: 'John', // 键名没有引号
'age': 30, // 单引号
hobbies: ['读书',], // 末尾逗号
// 这是注释 // 不允许注释
birthday: new Date() // 不支持Date对象
}
// ✅ 有效的JSON
{
"name": "John",
"age": 30,
"hobbies": ["读书"],
"birthday": "2024-01-01T00:00:00.000Z"
}
JSON格式化与美化
为什么需要格式化?
API返回的JSON通常是压缩的(minified),难以阅读:
{ "name": "张三", "age": 30, "skills": ["JavaScript", "Python"], "address": { "city": "北京" } }
格式化后:
{
"name": "张三",
"age": 30,
"skills": ["JavaScript", "Python"],
"address": {
"city": "北京"
}
}
JavaScript原生格式化
// JSON.stringify() 第三个参数控制缩进
const data = {
name: '张三',
age: 30,
skills: ['JavaScript', 'Python'],
};
// 美化输出(缩进2个空格)
console.log(JSON.stringify(data, null, 2));
// 美化输出(缩进4个空格)
console.log(JSON.stringify(data, null, 4));
// 使用制表符缩进
console.log(JSON.stringify(data, null, '\t'));
自定义格式化
function formatJSON(obj, options = {}) {
const {
indent = 2, // 缩进空格数
maxLineLength = 80, // 最大行长度
sortKeys = false, // 是否排序键名
} = options;
// 如果需要排序键名
if (sortKeys && typeof obj === 'object' && obj !== null) {
obj = sortObjectKeys(obj);
}
return JSON.stringify(obj, null, indent);
}
// 递归排序对象键名
function sortObjectKeys(obj) {
if (Array.isArray(obj)) {
return obj.map(sortObjectKeys);
}
if (typeof obj === 'object' && obj !== null) {
return Object.keys(obj)
.sort()
.reduce((sorted, key) => {
sorted[key] = sortObjectKeys(obj[key]);
return sorted;
}, {});
}
return obj;
}
// 使用
const data = { z: 1, a: 2, m: { y: 3, b: 4 } };
console.log(formatJSON(data, { sortKeys: true }));
// {
// "a": 2,
// "m": {
// "b": 4,
// "y": 3
// },
// "z": 1
// }
压缩JSON(Minify)
function minifyJSON(jsonString) {
try {
// 先解析再压缩,确保格式正确
const obj = JSON.parse(jsonString);
return JSON.stringify(obj);
} catch (error) {
throw new Error('Invalid JSON');
}
}
const formatted = `{
"name": "张三",
"age": 30
}`;
console.log(minifyJSON(formatted));
// {"name":"张三","age":30}
JSON验证与错误处理
常见JSON错误
// 1. 多余的逗号
const invalid1 = '{"name": "John",}'; // ❌
// 2. 单引号
const invalid2 = "{'name': 'John'}"; // ❌
// 3. 未转义的字符
const invalid3 = '{"message": "She said "Hi""}'; // ❌
// 4. 数字前导零
const invalid4 = '{"id": 001}'; // ❌
// 5. NaN或Infinity
const invalid5 = '{"value": NaN}'; // ❌
安全的JSON解析
function safeJSONParse(jsonString, fallback = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('JSON解析失败:', error.message);
return fallback;
}
}
// 使用
const result = safeJSONParse('invalid json', {});
console.log(result); // {}
// 更详细的错误处理
function parseJSONWithError(jsonString) {
try {
return {
success: true,
data: JSON.parse(jsonString),
error: null,
};
} catch (error) {
return {
success: false,
data: null,
error: {
message: error.message,
position: extractErrorPosition(error.message),
},
};
}
}
function extractErrorPosition(errorMessage) {
// Chrome: "Unexpected token } in JSON at position 25"
const match = errorMessage.match(/position (\d+)/);
return match ? parseInt(match[1]) : null;
}
// 使用
const result = parseJSONWithError('{"name": "John",}');
if (!result.success) {
console.log(`错误位置: ${result.error.position}`);
console.log(`错误信息: ${result.error.message}`);
}
JSON Schema验证
// 简化的JSON Schema验证器
function validateJSON(data, schema) {
const errors = [];
function validate(value, schemaNode, path = '') {
// 检查类型
if (schemaNode.type) {
const actualType = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value;
if (actualType !== schemaNode.type) {
errors.push({
path,
message: `Expected type ${schemaNode.type}, got ${actualType}`,
});
return;
}
}
// 检查必需字段
if (schemaNode.type === 'object' && schemaNode.required) {
schemaNode.required.forEach((key) => {
if (!(key in value)) {
errors.push({
path: `${path}.${key}`,
message: `Required field missing`,
});
}
});
}
// 递归验证对象属性
if (schemaNode.type === 'object' && schemaNode.properties) {
Object.keys(schemaNode.properties).forEach((key) => {
if (key in value) {
validate(value[key], schemaNode.properties[key], `${path}.${key}`);
}
});
}
// 递归验证数组元素
if (schemaNode.type === 'array' && schemaNode.items) {
value.forEach((item, index) => {
validate(item, schemaNode.items, `${path}[${index}]`);
});
}
}
validate(data, schema);
return {
valid: errors.length === 0,
errors,
};
}
// 使用示例
const schema = {
type: 'object',
required: ['name', 'age'],
properties: {
name: { type: 'string' },
age: { type: 'number' },
email: { type: 'string' },
},
};
const data = {
name: 'John',
age: '30', // 错误:应该是数字
};
const result = validateJSON(data, schema);
console.log(result);
// {
// valid: false,
// errors: [
// { path: '.age', message: 'Expected type number, got string' }
// ]
// }
JSON数据转换
JSON ↔️ CSV
// JSON转CSV
function jsonToCSV(jsonArray) {
if (!Array.isArray(jsonArray) || jsonArray.length === 0) {
return '';
}
// 获取所有键名作为表头
const headers = Object.keys(jsonArray[0]);
// 创建CSV行
const csvRows = [
headers.join(','), // 表头
...jsonArray.map((obj) =>
headers
.map((header) => {
const value = obj[header];
// 处理包含逗号或引号的值
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
})
.join(',')
),
];
return csvRows.join('\n');
}
// 使用
const users = [
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, city: 'Los Angeles' },
];
console.log(jsonToCSV(users));
// name,age,city
// John,30,New York
// Jane,25,Los Angeles
// CSV转JSON
function csvToJSON(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map((line) => {
const values = line.split(',');
return headers.reduce((obj, header, index) => {
obj[header.trim()] = values[index].trim();
return obj;
}, {});
});
}
JSON ↔️ XML
// JSON转XML(简化版)
function jsonToXML(obj, rootName = 'root') {
function convert(obj, name) {
if (Array.isArray(obj)) {
return obj.map((item) => convert(item, name)).join('');
}
if (typeof obj === 'object' && obj !== null) {
const children = Object.entries(obj)
.map(([key, value]) => convert(value, key))
.join('');
return `<${name}>${children}</${name}>`;
}
return `<${name}>${obj}</${name}>`;
}
return `<?xml version="1.0" encoding="UTF-8"?>\n${convert(obj, rootName)}`;
}
// 使用
const data = {
user: {
name: 'John',
age: 30,
skills: ['JavaScript', 'Python'],
},
};
console.log(jsonToXML(data));
// <?xml version="1.0" encoding="UTF-8"?>
// <root><user><name>John</name><age>30</age><skills>JavaScript</skills><skills>Python</skills></user></root>
深度合并JSON
function deepMerge(target, source) {
const output = { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!(key in target)) {
output[key] = source[key];
} else {
output[key] = deepMerge(target[key], source[key]);
}
} else {
output[key] = source[key];
}
});
}
return output;
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
// 使用
const defaults = {
theme: 'light',
colors: {
primary: 'blue',
secondary: 'green',
},
};
const userConfig = {
colors: {
primary: 'red',
},
fontSize: 16,
};
const config = deepMerge(defaults, userConfig);
console.log(config);
// {
// theme: 'light',
// colors: {
// primary: 'red', // 覆盖
// secondary: 'green' // 保留
// },
// fontSize: 16 // 新增
// }
处理大型JSON文件
回到开头那个 200MB 文件的教训。JSON.parse 的问题在于它要把整个文件先读进内存、再一次性解析成对象树——内存占用往往是原文件的 3 到 5 倍,而且整个过程同步阻塞,主线程一动不能动。几 MB 无所谓,上百 MB 标签页就废了。
破局思路只有一个:别一次性吃下去,一块一块地嚼。如果你的数据恰好是 NDJSON(每行一个独立 JSON 对象,日志和大导出几乎都长这样),流式解析就是救命稻草。
流式解析
// 使用Web Streams API处理大型JSON
async function processLargeJSON(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 处理完整的JSON对象
let boundary = buffer.lastIndexOf('}\n');
if (boundary !== -1) {
const chunk = buffer.substring(0, boundary + 1);
buffer = buffer.substring(boundary + 1);
// 处理chunk中的数据
chunk.split('\n').forEach((line) => {
if (line.trim()) {
try {
const obj = JSON.parse(line);
processItem(obj);
} catch (e) {
console.error('Parse error:', e);
}
}
});
}
}
}
function processItem(item) {
console.log('Processing:', item);
// 处理每个项目
}
分块读取
async function readJSONInChunks(file, chunkSize = 1024 * 1024) {
const fileSize = file.size;
let offset = 0;
while (offset < fileSize) {
const chunk = file.slice(offset, offset + chunkSize);
const text = await chunk.text();
// 处理chunk
processChunk(text);
offset += chunkSize;
}
}
// 使用FileReader逐行读取
function readJSONLineByLine(file, callback) {
const reader = new FileReader();
let lineBuffer = '';
reader.onload = (e) => {
const text = e.target.result;
const lines = (lineBuffer + text).split('\n');
// 保留最后不完整的行
lineBuffer = lines.pop();
lines.forEach((line) => {
if (line.trim()) {
try {
const obj = JSON.parse(line);
callback(obj);
} catch (err) {
console.error('Parse error:', err);
}
}
});
};
reader.readAsText(file);
}
JSON安全性
防止原型污染
// ❌ 危险:可能污染原型
function unsafeMerge(target, source) {
for (let key in source) {
target[key] = source[key];
}
return target;
}
// 攻击示例
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
unsafeMerge({}, malicious);
console.log({}.isAdmin); // true - 原型被污染!
// ✅ 安全:检查键名
function safeMerge(target, source) {
for (let key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // 跳过危险键名
}
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
安全的JSON解析
// 使用JSON.parse的reviver参数过滤危险内容
function secureJSONParse(jsonString) {
return JSON.parse(jsonString, (key, value) => {
// 过滤危险键名
if (key === '__proto__' || key === 'constructor') {
return undefined;
}
// 过滤可能的XSS
if (typeof value === 'string') {
// 移除潜在的脚本标签
value = value.replace(/<script[^>]*>.*?<\/script>/gi, '');
}
return value;
});
}
防止拒绝服务攻击(DoS)
// 限制JSON大小和深度
function parseJSONSafely(jsonString, options = {}) {
const {
maxSize = 10 * 1024 * 1024, // 最大10MB
maxDepth = 20, // 最大深度20层
} = options;
// 检查大小
if (jsonString.length > maxSize) {
throw new Error('JSON too large');
}
const obj = JSON.parse(jsonString);
// 检查深度
function checkDepth(obj, currentDepth = 0) {
if (currentDepth > maxDepth) {
throw new Error('JSON too deep');
}
if (typeof obj === 'object' && obj !== null) {
Object.values(obj).forEach((value) => {
checkDepth(value, currentDepth + 1);
});
}
}
checkDepth(obj);
return obj;
}
实战案例
案例1:API响应数据处理
async function fetchAndProcessAPI(url) {
try {
const response = await fetch(url);
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析JSON
const data = await response.json();
// 验证数据结构
if (!data.results || !Array.isArray(data.results)) {
throw new Error('Invalid API response structure');
}
// 转换数据格式
const processedData = data.results.map((item) => ({
id: item.id,
title: item.title || 'Untitled',
date: new Date(item.created_at).toLocaleDateString('zh-CN'),
tags: item.tags || [],
}));
return processedData;
} catch (error) {
console.error('API处理失败:', error);
throw error;
}
}
// 使用
fetchAndProcessAPI('https://api.example.com/data')
.then((data) => {
console.log('处理后的数据:', data);
})
.catch((error) => {
console.error('错误:', error);
});
案例2:配置文件管理
class ConfigManager {
constructor(defaultConfig = {}) {
this.config = defaultConfig;
}
// 加载配置
async load(url) {
try {
const response = await fetch(url);
const config = await response.json();
// 合并配置
this.config = deepMerge(this.config, config);
// 验证配置
this.validate();
return this.config;
} catch (error) {
console.error('配置加载失败:', error);
throw error;
}
}
// 验证配置
validate() {
const required = ['apiUrl', 'timeout', 'retryCount'];
required.forEach((key) => {
if (!(key in this.config)) {
throw new Error(`缺少必需配置项: ${key}`);
}
});
}
// 获取配置
get(key, defaultValue = null) {
return key
.split('.')
.reduce((obj, k) => (obj && obj[k] !== undefined ? obj[k] : defaultValue), this.config);
}
// 保存配置
save() {
const json = JSON.stringify(this.config, null, 2);
localStorage.setItem('app-config', json);
}
}
// 使用
const config = new ConfigManager({
apiUrl: 'https://api.example.com',
timeout: 5000,
retryCount: 3,
});
await config.load('/config.json');
console.log(config.get('apiUrl'));
console.log(config.get('features.darkMode', false));
案例3:数据导入导出工具
class DataExporter {
constructor(data) {
this.data = data;
}
// 导出为JSON
toJSON(pretty = true) {
return JSON.stringify(this.data, null, pretty ? 2 : 0);
}
// 导出为CSV
toCSV() {
return jsonToCSV(this.data);
}
// 导出为Excel(简化版,实际使用需要库支持)
toExcelData() {
if (!Array.isArray(this.data)) {
throw new Error('Data must be an array');
}
const headers = Object.keys(this.data[0]);
const rows = this.data.map((obj) => headers.map((h) => obj[h]));
return [headers, ...rows];
}
// 下载文件
download(filename, format = 'json') {
let content, mimeType;
switch (format) {
case 'json':
content = this.toJSON();
mimeType = 'application/json';
break;
case 'csv':
content = this.toCSV();
mimeType = 'text/csv';
break;
default:
throw new Error('Unsupported format');
}
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${filename}.${format}`;
a.click();
URL.revokeObjectURL(url);
}
}
// 使用
const data = [
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, city: 'Los Angeles' },
];
const exporter = new DataExporter(data);
exporter.download('users', 'json');
exporter.download('users', 'csv');
最后几句掏心窝子的话
写到这儿,与其再背一遍知识点,不如说几句我现在真正会挂在嘴边的判断:
大文件别一次性 JSON.parse,真的会卡死。 这是我用一个冻住的标签页换来的教训。只要文件上了几十 MB,先想清楚它是不是 NDJSON、能不能逐行流式读;是单个巨型对象那种,宁可在后端先拆好再传前端。
结构深就上 JSONPath,别手撸递归。 我有过为了取一个字段写了三层嵌套循环、还漏判 null 报错的黑历史。表达式一行能搞定的事,没必要把自己绕进去。唯一要注意的是路径写错它经常静默返回空,调的时候一段一段往里加。
外部来的 JSON 一律当不可信处理。 原型污染、深度炸弹这些不是教科书才有的东西,过滤 __proto__、限制大小和深度,几行代码的事,省得哪天上了线再回来补。
如果只是临时要格式化、转个 CSV、或者把一段乱七八糟的接口返回理顺,其实不用自己写脚本。下面这几个工具我自己也在用,全部在浏览器本地跑,数据不会上传到任何服务器——处理公司内部数据或者带敏感字段的导出时,这点我还挺在意的。
至于「保持 JSON 结构简单清晰」这种话——道理都懂,但接口是别人写的,你我说了不算,能在自己这一摊把数据接干净就不错了。共勉。
相关工具
- 📝 在线JSON格式化工具 - 实时格式化和验证
- 🔄 CSV/JSON互转工具 - 快速转换数据格式
- 🔐 Base64编码工具 - 编码二进制数据
关键词: JSON, JSON格式化, JSON验证, 数据转换, API处理
更新时间: 2026-01-05
我们是一支开发者小团队,负责构建和维护 ToolsForge —— 150+ 个完全在浏览器本地运行、隐私优先的工具。这些文章来自我们日常的工程实践,而不是纸上谈兵。
了解我们