← 中文资源

简体中文版本

RFC 6901 JSON Pointer 读写与删除

使用 RFC 6901 指针解析、赋值和删除 JSON 值,处理转义、数组索引与原型污染防护。

阅读英文原文

功能说明

该示例实现 JSON Pointer 的三个常用操作:按路径读取、在已有父级下写入、删除并返回被删值。指针支持空串表示整个文档,斜杠分隔层级,波浪号转义为 ~0、斜杠转义为 ~1。数组既接受规范十进制下标,也支持 set 时用减号追加。适用于配置覆盖、JSON Patch 基础设施与按路径更新嵌套数据。

关键代码

parseJsonPointer 先校验前导斜杠,再对每个 token 分别替换 ~1 与 ~0,顺序不能颠倒,否则 ~01 会被错误还原。get 逐层下探,遇到非对象、越界数组或非自有属性立即返回 undefined,不会顺着原型链取值。set 与 remove 在每一层调用 assertSegment,拒绝 proto、prototype 与 constructor,避免通过路径写入污染 Object.prototype。数组下标禁止前导零,set 允许等于长度以实现追加。

使用注意

set 不会自动创建中间对象,父级缺失会抛 RangeError,这能防止拼写错误悄悄生成新分支;需要自动建链时应在上层显式创建。删除数组元素使用 splice,后续下标会前移,批量删除应从后往前处理。读取不会触发原型污染防护,但只访问自有属性,因此 constructor 等继承字段不会被读到。该实现不支持 URI fragment 形式的前导井号。

验证方式

测试转义路径 /a1b 与 /m0n 能命中对应键;对数组执行读取、覆盖与减号追加;删除数组元素后断言长度减一;向 set 传入 proto 路径应抛 TypeError;读取不存在路径返回 undefined;空指针读取返回根文档,而对空指针执行 set 或 remove 应抛错。

代码示例

const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);

export function parseJsonPointer(pointer) {
  if (typeof pointer !== "string") throw new TypeError("pointer must be a string");
  if (pointer === "") return [];
  if (!pointer.startsWith("/")) throw new SyntaxError("pointer must start with /");
  return pointer
    .slice(1)
    .split("/")
    .map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
}

function assertSegment(token) {
  if (BLOCKED.has(token)) throw new TypeError("unsafe pointer segment: " + token);
}

function arrayIndex(token, allowAppend, length) {
  if (token === "-") {
    if (!allowAppend) throw new RangeError("append is only allowed for set");
    return length;
  }
  if (!/^(0|[1-9]\d*)$/.test(token)) throw new RangeError("invalid array index: " + token);
  const index = Number(token);
  const max = allowAppend ? length : length - 1;
  if (index > max) throw new RangeError("array index out of range: " + token);
  return index;
}

export function getJsonPointer(document, pointer) {
  let current = document;
  for (const token of parseJsonPointer(pointer)) {
    if (current === null || typeof current !== "object") return undefined;
    if (Array.isArray(current)) {
      if (!/^(0|[1-9]\d*)$/.test(token)) return undefined;
      const index = Number(token);
      if (index >= current.length) return undefined;
      current = current[index];
    } else {
      if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
      current = current[token];
    }
  }
  return current;
}

export function setJsonPointer(document, pointer, value) {
  const tokens = parseJsonPointer(pointer);
  if (tokens.length === 0) throw new TypeError("cannot set the document root");
  let current = document;
  for (let index = 0; index < tokens.length - 1; index += 1) {
    const token = tokens[index];
    assertSegment(token);
    if (current === null || typeof current !== "object") {
      throw new TypeError("parent path does not exist");
    }
    if (Array.isArray(current)) {
      current = current[arrayIndex(token, false, current.length)];
    } else {
      if (!Object.prototype.hasOwnProperty.call(current, token)) {
        throw new RangeError("missing path segment: " + token);
      }
      current = current[token];
    }
  }
  const last = tokens[tokens.length - 1];
  assertSegment(last);
  if (current === null || typeof current !== "object") {
    throw new TypeError("parent path does not exist");
  }
  if (Array.isArray(current)) {
    const index = arrayIndex(last, true, current.length);
    if (index === current.length) current.push(value);
    else current[index] = value;
  } else {
    current[last] = value;
  }
  return document;
}

export function removeJsonPointer(document, pointer) {
  const tokens = parseJsonPointer(pointer);
  if (tokens.length === 0) throw new TypeError("cannot remove the document root");
  let current = document;
  for (let index = 0; index < tokens.length - 1; index += 1) {
    const token = tokens[index];
    assertSegment(token);
    if (current === null || typeof current !== "object") return undefined;
    if (Array.isArray(current)) {
      const position = arrayIndex(token, false, current.length);
      current = current[position];
    } else {
      if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
      current = current[token];
    }
  }
  const last = tokens[tokens.length - 1];
  assertSegment(last);
  if (current === null || typeof current !== "object") return undefined;
  if (Array.isArray(current)) {
    const position = arrayIndex(last, false, current.length);
    return current.splice(position, 1)[0];
  }
  if (!Object.prototype.hasOwnProperty.call(current, last)) return undefined;
  const removed = current[last];
  delete current[last];
  return removed;
}