JavaScript JSON 白名单:只想筛顶层字段,为什么嵌套对象也变成空了
导出一份任务数据时,只想保留顶层job和rows,于是把这两个名字放进JSON.stringify的第二个参数。结果两个顶层字段都在,job里面却变成空对象,rows中的对象也丢了字段。数组形式的replacer是一份贯穿序列化过程的属性名清单,不是仅针对根对象的选择器。
完整程序已在Node.js v24.19.0运行。保存为demo.mjs后执行node demo.mjs,只对固定内存数据编码和解析,不会把任何内容传给服务。示例使用普通短小对象,随后增加一个不可枚举字段作为独立边界样本。
AI模型生成概念图:相同筛子在多个容器层级重复使用,而一排数组槽位仍保留,用于比喻属性清单的作用范围;不是JSON解析器界面。
完整实验程序
source同时包含顶层label、job内的label,以及数组中的对象和数字。这样可以分别核对对象属性名、数组位置和原始输入,避免把编码后少字段误判成源对象被删除。
完整可运行程序
import assert from 'node:assert/strict';
const source = {
job: { id: 7, label: 'ready' },
rows: [{ id: 1, label: 'first' }, 5],
label: 'top'
};
const narrow = JSON.stringify(source, ['job', 'rows']);
assert.equal(narrow, '{"job":{},"rows":[{},5]}');
console.log('container names only:', narrow);
const expanded = JSON.stringify(source, ['job', 'rows', 'id']);
assert.equal(expanded, '{"job":{"id":7},"rows":[{"id":1},5]}');
assert.equal(JSON.parse(expanded).rows.length, 2);
console.log('including id:', expanded);
const selected = { job: source.job, rows: source.rows };
const topOnly = JSON.stringify(selected);
assert.equal(topOnly,
'{"job":{"id":7,"label":"ready"},"rows":[{"id":1,"label":"first"},5]}');
console.log('select top level first:', topOnly);
const hidden = {};
Object.defineProperty(hidden, 'id', { value: 9, enumerable: false });
assert.equal(JSON.stringify(hidden), '{}');
assert.equal(JSON.stringify(hidden, ['id']), '{"id":9}');
console.log('listed non-enumerable:', JSON.stringify(hidden, ['id']));
assert.equal(source.label, 'top');
assert.equal(source.job.label, 'ready');
console.log('source still intact:', true);本次实际输出
container names only: {"job":{},"rows":[{},5]}
including id: {"job":{"id":7},"rows":[{"id":1},5]}
select top level first: {"job":{"id":7,"label":"ready"},"rows":[{"id":1,"label":"first"},5]}
listed non-enumerable: {"id":9}
source still intact: true名称清单会继续进入每个对象
第一行container names only里,job对应{},rows对应[{},5]。根对象允许job和rows,但进入job后,id和label都不在同一清单中,于是没有可输出属性。数组中的第一个对象也接受同样规则,因此同样成为{}。
第二行把id加入清单,两个嵌套对象的id都回来了。这个添加无法表达“只保留job.id,但保留rows里的全部字段”一类按路径变化的规则。它表达的是可使用哪些属性名,并不包含字段所处的完整路径。
数组位置没有按这份名字清单过滤
rows仍然有两项,而且数字五保留,虽然清单里没有字符串零和一。数组序列化沿索引位置处理成员,不使用对象那种属性名列表来挑选数组下标;当某一项本身又是对象时,才继续应用对象属性规则。
所以,给清单加一个字符串零不能当作通用的“只导出数组第一项”功能。需要删选成员时,应先按业务条件得到目标数组,再执行序列化。若数组位置与其他记录存在对应关系,还应先决定过滤以后如何保留稳定标识。
仅筛顶层时,先构造明确的导出对象
第三行select top level first先建立只含job、rows的新对象,再使用默认JSON.stringify。结果没有顶层label,但两个嵌套对象的label都保留。这正是本例最初想表达的导出范围,代码也直接写出了允许的顶层字段。
selected仍引用原来的job和rows,并不是深拷贝。由于本例只立即编码,不在编码前修改嵌套内容,浅层选择已经足够;若要编辑候选导出对象,就需另外设计复制或重建规则。不要把一次字段选择扩大理解成整份数据隔离。
白名单不等于只读取默认可枚举字段
listed non-enumerable输出{"id":9},而同一hidden对象默认编码为{}。显式PropertyList会按列出的名称取值,因此能读到本例不可枚举的id。它并不是在默认Object.keys结果上简单做一次交集。
这也意味着应把序列化对象限定为自己理解的数据结构。属性读取和自定义toJSON都可能执行代码,字段白名单不能当作安全沙箱。最后两条断言确认示例的source仍有原来的label,source still intact为true;本例的字段消失发生在输出文本中,未修改输入。
如果导出规则涉及不同路径、类型或字段之间的依赖,显式构造传输对象通常更容易验收。测试应解析最终JSON,逐项比较结构,至少覆盖嵌套对象、含对象的数组、原始值成员,以及业务必须保留的字段。
参考资料
官方资料核验日期:2026年10月2日。上述输出来自本文完整程序的本地执行,断言全部通过,退出状态为零。


